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