返回 DeepSeek-Reasonix
SearchSourcesPanel.tsx
根目录 / desktop / frontend / src / components / SearchSourcesPanel.tsx
1 import { useState } from "react";
2 import { ChevronRight } from "lucide-react";
3 import { useT } from "../lib/i18n";
4 import { normalizeSearchSources } from "../lib/searchSourcesPresentation";
5 import type { SearchSource } from "../lib/searchSources";
6
7 export function SearchSourcesPanel({ sources }: { sources?: SearchSource[] }) {
8 const t = useT();
9 const [open, setOpen] = useState(false);
10 const presentation = normalizeSearchSources(sources);
11 if (presentation.visible.length === 0) return null;
12
13 const countLabel = t("sources.count", { n: presentation.visible.length });
14 const hiddenLabel = presentation.hiddenCount > 0 ? ` · ${t("sources.hidden", { n: presentation.hiddenCount })}` : "";
15
16 return (
17 <section className="msg-search-sources" aria-label={t("sources.title")}>
18 <button
19 type="button"
20 className="msg-search-sources__toggle"
21 aria-expanded={open}
22 onKeyDown={(event) => {
23 if (event.key === "Escape" && open) {
24 event.preventDefault();
25 setOpen(false);
26 }
27 }}
28 onClick={() => setOpen((value) => !value)}
29 >
30 <ChevronRight className={`msg-search-sources__chevron${open ? " msg-search-sources__chevron--open" : ""}`} size={15} aria-hidden="true" />
31 <span>{t("sources.title")} · {countLabel}{hiddenLabel}</span>
32 </button>
33 {open && (
34 <div className="msg-search-sources__body">
35 {presentation.visible.map((source, index) => (
36 <div className="msg-search-source" key={`${source.canonicalUrl}-${index}`}>
37 <a
38 className="msg-search-source__link"
39 href={source.href}
40 target="_blank"
41 rel="noreferrer noopener"
42 title={t("sources.open")}
43 >
44 <span className="msg-search-source__title">{source.title}</span>
45 <span className="msg-search-source__url">
46 <span>{source.displayUrl}</span>
47 <span aria-hidden="true">↗</span>
48 </span>
49 </a>
50 <button
51 type="button"
52 className="msg-search-source__copy"
53 aria-label={t("sources.copyLink")}
54 title={t("sources.copyLink")}
55 onClick={(event) => {
56 event.preventDefault();
57 event.stopPropagation();
58 void navigator.clipboard?.writeText(source.href);
59 }}
60 >
61 <span aria-hidden="true">⧉</span>
62 </button>
63 </div>
64 ))}
65 </div>
66 )}
67 </section>
68 );
69 }
70
70 lines Plain Text