feat: add ITTO text highlight filter

This commit is contained in:
ittoview
2026-05-23 03:39:40 +01:00
parent 8ca1d820aa
commit d3ae9697ef

View File

@@ -1,6 +1,6 @@
import { useMemo, useState } from 'react'
import { motion } from 'framer-motion'
import { FileOutput, FileText, Wrench } from 'lucide-react'
import { FileOutput, FileText, Wrench, X } from 'lucide-react'
import {
artifactMap,
knowledgeAreas,
@@ -12,12 +12,17 @@ import type { Process, ProcessRef } from '@/types/itto'
type ViewKey = 'inputs' | 'tools' | 'outputs'
type CollectionItem = {
label: string
details: string[]
}
type CollectionRow = {
processId: string
processCode: string
processName: string
processNameEn: string
items: string[]
items: CollectionItem[]
}
type CollectionArea = {
@@ -45,14 +50,13 @@ const itemVariants = {
visible: { opacity: 1, y: 0 },
}
function formatRef(ref: ProcessRef, viewKey: ViewKey) {
function formatRef(ref: ProcessRef, viewKey: ViewKey): CollectionItem {
const normalized = normalizeProcessRef(ref)
const entity = viewKey === 'tools' ? toolMap.get(normalized.id) : artifactMap.get(normalized.id)
const name = entity?.name ?? normalized.id
const detail = normalized.detail?.map((item) => item.label).filter(Boolean) ?? []
const label = entity?.name ?? normalized.id
const details = normalized.detail?.map((item) => item.label).filter(Boolean) ?? []
if (!detail.length) return name
return `${name}${detail.join('、')}`
return { label, details }
}
function getRefsByView(process: Process, viewKey: ViewKey) {
@@ -61,8 +65,67 @@ function getRefsByView(process: Process, viewKey: ViewKey) {
return process.outputs
}
function uniqueItems(items: string[]) {
return Array.from(new Set(items))
function uniqueItems(items: CollectionItem[]) {
const seen = new Set<string>()
return items.filter((item) => {
const key = `${item.label}__${item.details.join('、')}`
if (seen.has(key)) return false
seen.add(key)
return true
})
}
function itemMatchesSelected(item: CollectionItem, selectedText: string | null) {
if (!selectedText) return false
return item.label === selectedText || item.details.includes(selectedText)
}
function renderSelectableText(
text: string,
selectedText: string | null,
onSelect: (text: string) => void
) {
const isSelected = selectedText === text
return (
<button
type="button"
aria-pressed={isSelected}
onClick={() => onSelect(text)}
className={`mx-[-2px] rounded px-0.5 text-left transition-colors hover:bg-indigo-50 hover:text-indigo-600 dark:hover:bg-indigo-900/40 dark:hover:text-indigo-300 ${
isSelected
? 'bg-yellow-200 text-yellow-950 ring-1 ring-yellow-300 dark:bg-yellow-500/30 dark:text-yellow-100 dark:ring-yellow-500/40'
: 'text-gray-700 dark:text-gray-300'
}`}
>
{text}
</button>
)
}
function renderCollectionItems(
items: CollectionItem[],
selectedText: string | null,
onSelect: (text: string) => void
) {
return items.map((item, itemIndex) => (
<span key={`${item.label}-${item.details.join('-')}-${itemIndex}`}>
{itemIndex > 0 && <span className="text-gray-400 dark:text-gray-500"></span>}
{renderSelectableText(item.label, selectedText, onSelect)}
{item.details.length > 0 && (
<span>
<span className="text-gray-500 dark:text-gray-400"></span>
{item.details.map((detail, detailIndex) => (
<span key={`${item.label}-${detail}-${detailIndex}`}>
{detailIndex > 0 && <span className="text-gray-400 dark:text-gray-500"></span>}
{renderSelectableText(detail, selectedText, onSelect)}
</span>
))}
<span className="text-gray-500 dark:text-gray-400"></span>
</span>
)}
</span>
))
}
function buildCollection(viewKey: ViewKey): CollectionArea[] {
@@ -91,6 +154,7 @@ function buildCollection(viewKey: ViewKey): CollectionArea[] {
export function IttoCollectionsPage() {
const [activeTab, setActiveTab] = useState<ViewKey>('inputs')
const [selectedText, setSelectedText] = useState<string | null>(null)
const collection = useMemo(() => buildCollection(activeTab), [activeTab])
const activeLabel = tabs.find((tab) => tab.key === activeTab)?.label
@@ -124,6 +188,20 @@ export function IttoCollectionsPage() {
</div>
</div>
{selectedText && (
<div className="flex flex-wrap items-center gap-2 rounded-xl bg-yellow-50 px-4 py-3 text-sm text-yellow-900 ring-1 ring-yellow-200 dark:bg-yellow-500/10 dark:text-yellow-100 dark:ring-yellow-500/30">
<span className="font-medium">{selectedText}</span>
<button
type="button"
onClick={() => setSelectedText(null)}
className="inline-flex h-6 w-6 items-center justify-center rounded-full text-yellow-700 transition-colors hover:bg-yellow-100 hover:text-yellow-900 dark:text-yellow-200 dark:hover:bg-yellow-500/20"
aria-label="清除标签"
>
<X size={14} />
</button>
</div>
)}
<motion.div
key={activeTab}
variants={containerVariants}
@@ -166,27 +244,35 @@ export function IttoCollectionsPage() {
</tr>
</thead>
<tbody className="divide-y divide-gray-100 dark:divide-gray-700">
{area.rows.map((row) => (
<tr key={row.processId} className="align-top">
<td className="px-4 py-3">
<div className="flex items-start gap-2">
<span
className="mt-0.5 inline-flex shrink-0 rounded-md px-2 py-0.5 text-xs font-semibold text-white"
style={{ backgroundColor: area.color }}
>
{row.processCode}
</span>
<div className="min-w-0">
<div className="text-sm font-medium text-gray-900 dark:text-white">{row.processName}</div>
<div className="text-xs text-gray-500 dark:text-gray-400">{row.processNameEn}</div>
{area.rows.map((row) => {
const rowMatched = row.items.some((item) => itemMatchesSelected(item, selectedText))
return (
<tr
key={row.processId}
className={`align-top transition-colors ${
rowMatched ? 'bg-yellow-50/60 dark:bg-yellow-500/5' : ''
}`}
>
<td className="px-4 py-3">
<div className="flex items-start gap-2">
<span
className="mt-0.5 inline-flex shrink-0 rounded-md px-2 py-0.5 text-xs font-semibold text-white"
style={{ backgroundColor: area.color }}
>
{row.processCode}
</span>
<div className="min-w-0">
<div className="text-sm font-medium text-gray-900 dark:text-white">{row.processName}</div>
<div className="text-xs text-gray-500 dark:text-gray-400">{row.processNameEn}</div>
</div>
</div>
</div>
</td>
<td className="px-4 py-3 text-sm leading-6 text-gray-700 dark:text-gray-300">
{row.items.join('')}
</td>
</tr>
))}
</td>
<td className="px-4 py-3 text-sm leading-6 text-gray-700 dark:text-gray-300">
{renderCollectionItems(row.items, selectedText, setSelectedText)}
</td>
</tr>
)
})}
</tbody>
</table>
</div>