Merge branch 'dhk-1' of http://192.168.0.200/knowledge-base-bj/deerflow-server into dhk-1
This commit is contained in:
commit
a35ed77ff0
7 files changed
+693
-219
No files matched your search
@@ -27,6 +27,7 @@ import { Button } from "@/components/ui/button";
|
|||||||
import { toast } from "sonner";
|
import { toast } from "sonner";
|
||||||
import { getStoredAuth } from "@/core/auth";
|
import { getStoredAuth } from "@/core/auth";
|
||||||
import { queryClient } from "@/components/query-client-provider";
|
import { queryClient } from "@/components/query-client-provider";
|
||||||
|
import { Checkbox } from "@/components/ui/checkbox";
|
||||||
import Empty from "./empty";
|
import Empty from "./empty";
|
||||||
import { NetworkActor } from "./network-actor";
|
import { NetworkActor } from "./network-actor";
|
||||||
|
|
||||||
@@ -693,169 +694,402 @@ const DocumentUploadPopupContent: FC<{
|
|||||||
{/* A系统基因库 */}
|
{/* A系统基因库 */}
|
||||||
{activeTab === "A" && (
|
{activeTab === "A" && (
|
||||||
<div className="bg-white rounded-lg">
|
<div className="bg-white rounded-lg">
|
||||||
<div className="custom-radio-group pt-[10px] ml-[10px]">
|
{false && (
|
||||||
{searchTypes.map((option) => (
|
<div className="relative pt-[10px]">
|
||||||
<div
|
<div className="relative rounded-lg border border-neutral-200 bg-white dark-theme-input ml-[10px] mr-[10px] mb-[10px] transition-colors hover:border-neutral-300">
|
||||||
key={option.value}
|
<Textarea
|
||||||
className={`custom-radio-btn ${searchType === option.value ? "active" : ""}`}
|
placeholder="搜索资料..."
|
||||||
onClick={() => searchTypeChange(option.value)}
|
value={searchText}
|
||||||
>
|
onChange={(e) => setSearchText(e.target.value)}
|
||||||
{option.label}
|
onKeyDown={async (e) => {
|
||||||
</div>
|
if (e.key === "Enter" && !e.shiftKey) {
|
||||||
))}
|
e.preventDefault();
|
||||||
</div>
|
await searchAChange(searchType);
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
className=" max-h-[160px] resize-none border-0 dark-theme-input bg-transparent px-10 py-3 pr-20 text-sm text-neutral-700 placeholder:text-neutral-400 focus-visible:ring-0 focus-visible:outline-none"
|
||||||
|
rows={1}
|
||||||
|
/>
|
||||||
|
|
||||||
|
{/* 左侧搜索图标 */}
|
||||||
|
<div className="absolute left-3 top-3 text-neutral-400 pointer-events-none dark-theme-search">
|
||||||
|
<Search className="h-4 w-4" strokeWidth={1.5} />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* 右侧搜索按钮 */}
|
||||||
|
<Button
|
||||||
|
size="sm"
|
||||||
|
onClick={async () =>
|
||||||
|
await searchAChange(searchType)
|
||||||
|
}
|
||||||
|
disabled={isSearching || !searchText.trim()}
|
||||||
|
className="absolute right-2 top-2 h-7 rounded-md bg-neutral-900 px-3 text-xs font-medium text-white dark-theme-search transition-colors hover:bg-neutral-800 disabled:pointer-events-none disabled:opacity-40"
|
||||||
|
>
|
||||||
|
{isSearching ? (
|
||||||
|
<Loader2 className="h-3.5 w-3.5 animate-spin" />
|
||||||
|
) : (
|
||||||
|
<span>搜索</span>
|
||||||
|
)}
|
||||||
|
</Button>
|
||||||
|
|
||||||
|
{/* 左下角模式选择 */}
|
||||||
|
<div className="absolute left-2 bottom-1.5 flex items-center">
|
||||||
|
{/* <Select value={searchType} onValueChange={(value) => searchTypeChange(value)}>
|
||||||
|
<SelectTrigger className="h-6 w-auto min-w-[80px] gap-1.5 rounded border border-neutral-200 bg-white px-2 py-0 pr-1.5 text-[11px] font-medium text-neutral-600 hover:bg-neutral-50 data-[state=open]:bg-white">
|
||||||
|
<SelectValue />
|
||||||
|
</SelectTrigger>
|
||||||
|
<SelectContent align="start" className="min-w-[120px] rounded-md border border-neutral-200 bg-white p-0.5 shadow-sm">
|
||||||
|
{searchTypes.map((mode) => {
|
||||||
|
return (
|
||||||
|
<SelectItem
|
||||||
|
key={mode.value}
|
||||||
|
value={mode.value}
|
||||||
|
className="rounded px-2 py-1.5 text-[11px] font-medium data-[highlighted]:bg-neutral-100"
|
||||||
|
>
|
||||||
|
<div className="flex items-center gap-1.5">
|
||||||
|
<span className="text-neutral-700">{mode.label}</span>
|
||||||
|
</div>
|
||||||
|
</SelectItem>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</SelectContent>
|
||||||
|
</Select> */}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
<div className="custom-radio-group pt-[10px] ml-[10px]">
|
||||||
|
{" "}
|
||||||
|
{/* 外层容器,模拟 nz-radio-group */}
|
||||||
|
{searchTypes.map((option) => {
|
||||||
|
const isActive = searchType === option.value;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
key={option.value}
|
||||||
|
className={`custom-radio-btn ${isActive ? "active" : ""
|
||||||
|
}`}
|
||||||
|
onClick={() => searchTypeChange(option.value)}
|
||||||
|
>
|
||||||
|
{option.label}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
{/* 批量操作栏 - 当有选中项时显示 */}
|
||||||
{selectedSearchSources.length > 0 && (
|
{selectedSearchSources.length > 0 && (
|
||||||
<div className="flex items-center justify-between px-4 py-2 bg-gray-50 border-b border-gray-100">
|
<div className="flex items-center justify-between px-4 py-2 bg-gray-50 border-b border-gray-100">
|
||||||
<span className="text-sm text-gray-600">已选中 {selectedSearchSources.length} 项</span>
|
<span className="text-sm text-gray-600">
|
||||||
|
已选中 {selectedSearchSources.length} 项
|
||||||
|
</span>
|
||||||
<div className="flex gap-2">
|
<div className="flex gap-2">
|
||||||
<Button size="sm" variant="outline" onClick={handleBatchDelete} className="h-8 px-3 text-sm">
|
<Button
|
||||||
|
size="sm"
|
||||||
|
variant="outline"
|
||||||
|
onClick={handleBatchDelete}
|
||||||
|
className="h-8 px-3 text-sm"
|
||||||
|
>
|
||||||
<Trash2 className="h-3.5 w-3.5 mr-1" />
|
<Trash2 className="h-3.5 w-3.5 mr-1" />
|
||||||
删除选中
|
删除选中
|
||||||
</Button>
|
</Button>
|
||||||
<Button
|
<Button
|
||||||
size="sm"
|
size="sm"
|
||||||
onClick={() => void handleImport()}
|
onClick={() => handleImport()}
|
||||||
disabled={isImporting}
|
disabled={isImporting}
|
||||||
className="h-8 px-3 text-sm bg-blue-50 hover:bg-blue-100 text-blue-600 border border-blue-200"
|
className="h-8 px-3 text-sm bg-blue-50 hover:bg-blue-100 text-blue-600 border border-blue-200"
|
||||||
>
|
>
|
||||||
{isImporting ? <Loader2 className="h-3.5 w-3.5 mr-1 animate-spin" /> : <Plus className="h-3.5 w-3.5 mr-1" />}
|
{isImporting ? (
|
||||||
|
<Loader2 className="h-3.5 w-3.5 mr-1 animate-spin" />
|
||||||
|
) : (
|
||||||
|
<Plus className="h-3.5 w-3.5 mr-1" />
|
||||||
|
)}
|
||||||
导入选中
|
导入选中
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
{/* 如果没有选中项,显示普通导入按钮 */}
|
||||||
|
{isImporting && (
|
||||||
|
<div className="px-4 py-2 border-b border-gray-100">
|
||||||
|
<Button
|
||||||
|
onClick={() => handleImport()}
|
||||||
|
disabled={isImporting}
|
||||||
|
className="w-full h-8 text-sm bg-blue-50 hover:bg-blue-100 text-blue-600 border border-blue-200 disabled:opacity-50 disabled:cursor-not-allowed"
|
||||||
|
>
|
||||||
|
{isImporting ? (
|
||||||
|
<Loader2 className="h-4 w-4 mr-1 animate-spin" />
|
||||||
|
) : (
|
||||||
|
<Plus className="h-4 w-4 mr-1" />
|
||||||
|
)}
|
||||||
|
{isImporting ? "导入中..." : "导入全部"}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
{listData.length === 0 && <Empty />}
|
{listData.length === 0 && <Empty />}
|
||||||
|
|
||||||
{searchType === "human" && (
|
{searchType === "human" && (
|
||||||
<div className="bg-white p-[10px] grid grid-cols-5 gap-[10px]">
|
<div className="bg-white p-[10px] grid grid-cols-5 gap-[10px]">
|
||||||
{listData.map((item, index) => (
|
{listData.map((item: listDataItem, index: number) => {
|
||||||
<div key={index} className="relative flex items-center shadow-[0px_0px_2px_1px_#ededed] mb-[10px] p-[10px] rounded-[5px]">
|
return (
|
||||||
{/* 上传文件显示 loading 状态 */}
|
<div
|
||||||
{item.is_uploaded_file && item.processing_status === 'processing' && (
|
key={index}
|
||||||
<div className="absolute inset-0 bg-white/80 flex items-center justify-center z-10">
|
className="relative flex items-center shadow-[0px_0px_2px_1px_#ededed] mb-[10px] p-[10px] rounded-[5px]"
|
||||||
<Loader2 className="h-6 w-6 animate-spin text-primary" />
|
>
|
||||||
|
<div className="w-[50px] mr-[10px]">
|
||||||
|
<img
|
||||||
|
src={item.imagePath || ""}
|
||||||
|
className="w-[50px] h-[50px]"
|
||||||
|
alt=""
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
)}
|
<div className="flex-1 min-w-0">
|
||||||
{/* 上传文件完成状态标记 */}
|
<p
|
||||||
{item.is_uploaded_file && item.processing_status === 'error' && (
|
className="truncate hover:underline cursor-pointer"
|
||||||
<div className="absolute top-1 left-1">
|
onClick={() => linkToA(item.url)}
|
||||||
<span className="text-xs text-red-500">处理失败</span>
|
>
|
||||||
|
{item.name}
|
||||||
|
</p>
|
||||||
|
<p className="truncate text-[#909090]">
|
||||||
|
职业:{item.occupationData}
|
||||||
|
</p>
|
||||||
|
<p className="truncate text-[#909090]">
|
||||||
|
属地:{item.country}
|
||||||
|
</p>
|
||||||
</div>
|
</div>
|
||||||
)}
|
<div className="absolute top-[5px] right-[10px]">
|
||||||
{item.is_uploaded_file && item.processing_status === 'success' && (
|
<Checkbox
|
||||||
<div className="absolute top-1 left-1">
|
checked={item.checked}
|
||||||
<span className="text-xs text-green-500">✓ 已完成</span>
|
onCheckedChange={() =>
|
||||||
|
listDataCheckedChange(index, !item.checked)
|
||||||
|
}
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
)}
|
|
||||||
<div className="w-[50px] mr-[10px]">
|
|
||||||
<img src={item.imagePath || ""} className="w-[50px] h-[50px]" alt="" />
|
|
||||||
</div>
|
</div>
|
||||||
<div className="flex-1 min-w-0">
|
);
|
||||||
<p className="truncate hover:underline cursor-pointer" onClick={() => linkToA(item.url)}>{item.name}</p>
|
})}
|
||||||
<p className="truncate text-[#909090]">职业:{item.occupationData}</p>
|
|
||||||
<p className="truncate text-[#909090]">属地:{item.country}</p>
|
|
||||||
</div>
|
|
||||||
<div className="absolute top-[5px] right-[10px]">
|
|
||||||
<input type="checkbox" checked={item.checked} onChange={() => listDataCheckedChange(index, !item.checked)} className="w-4 h-4 cursor-pointer accent-primary" />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
))}
|
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
{searchType === "organization" && (
|
{searchType === "organization" && (
|
||||||
<div className="bg-white p-[10px] grid grid-cols-5 gap-[10px]">
|
<div className="bg-white p-[10px] grid grid-cols-5 gap-[10px]">
|
||||||
{listData.map((item, index) => (
|
{listData.map((item: listDataItem, index: number) => {
|
||||||
<div key={index} className="relative flex items-center shadow-[0px_0px_2px_1px_#ededed] mb-[10px] p-[10px] rounded-[5px]">
|
return (
|
||||||
<div className="w-[50px] mr-[10px]">
|
<div
|
||||||
<img src={item.imagePath || ""} className="w-[50px] h-[50px]" alt="" />
|
key={index}
|
||||||
|
className="relative flex items-center shadow-[0px_0px_2px_1px_#ededed] mb-[10px] p-[10px] rounded-[5px]"
|
||||||
|
>
|
||||||
|
<div className="w-[50px] mr-[10px]">
|
||||||
|
<img
|
||||||
|
src={item.imagePath || ""}
|
||||||
|
className="w-[50px] h-[50px]"
|
||||||
|
alt=""
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className="flex-1 min-w-0">
|
||||||
|
<p
|
||||||
|
className="truncate hover:underline cursor-pointer"
|
||||||
|
onClick={() => linkToA(item.url)}
|
||||||
|
>
|
||||||
|
{item.name}
|
||||||
|
</p>
|
||||||
|
<p className="truncate text-[#909090]">
|
||||||
|
组织类别:{item.subtype}
|
||||||
|
</p>
|
||||||
|
<p className="truncate text-[#909090]">
|
||||||
|
简介:{item.text}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<div className="absolute top-[5px] right-[10px]">
|
||||||
|
<Checkbox
|
||||||
|
checked={item.checked}
|
||||||
|
onCheckedChange={() =>
|
||||||
|
listDataCheckedChange(index, !item.checked)
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex-1 min-w-0">
|
);
|
||||||
<p className="truncate hover:underline cursor-pointer" onClick={() => linkToA(item.url)}>{item.name}</p>
|
})}
|
||||||
<p className="truncate text-[#909090]">组织类别:{item.subtype}</p>
|
|
||||||
<p className="truncate text-[#909090]">简介:{item.text}</p>
|
|
||||||
</div>
|
|
||||||
<div className="absolute top-[5px] right-[10px]">
|
|
||||||
<input type="checkbox" checked={item.checked} onChange={() => listDataCheckedChange(index, !item.checked)} className="w-4 h-4 cursor-pointer accent-primary" />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
))}
|
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
{searchType === "community" && (
|
{searchType === "community" && (
|
||||||
<div className="bg-white p-[10px] grid grid-cols-4 gap-[10px]">
|
<div className="bg-white p-[10px] grid grid-cols-4 gap-[10px]">
|
||||||
{listData.map((item, index) => (
|
{listData.map((item: listDataItem, index: number) => {
|
||||||
<div key={index} className="relative shadow-[0px_0px_2px_1px_#ededed] mb-[10px] p-[10px] rounded-[5px]">
|
return (
|
||||||
<div>
|
<div
|
||||||
<div className="truncate mb-[5px] hover:underline cursor-pointer" onClick={() => linkToA(item.url)}>{item.title}</div>
|
key={index}
|
||||||
<div className="flex items-center text-[#909090] mb-[5px]">
|
className="relative shadow-[0px_0px_2px_1px_#ededed] mb-[10px] p-[10px] rounded-[5px]"
|
||||||
{item.nations && <span className="truncate max-w-[200px] inline-block border border-[#cf1322] rounded px-1 text-[#cf1322]">{item.nations}</span>}
|
>
|
||||||
{item.labels && <span className="inline-block border border-[#bbb] rounded px-1 ml-[10px]">{item.labels}</span>}
|
<div>
|
||||||
|
<div
|
||||||
|
className="truncate mb-[5px] hover:underline cursor-pointer"
|
||||||
|
onClick={() => linkToA(item.url)}
|
||||||
|
>
|
||||||
|
{item.title}
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center text-[#909090] mb-[5px]">
|
||||||
|
{item.nations && (
|
||||||
|
<span className="truncate max-width-[200px] inline-block border border-[#cf1322] rounded px-1 text-[#cf1322]">
|
||||||
|
{item.nations}
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
{item.labels && (
|
||||||
|
<span className="inline-block border border-[#bbb] rounded px-1 ml-[10px]">
|
||||||
|
{item.labels}
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
<div className="flex justify-between mb-[5px]">
|
||||||
|
<span className="text-[#909090]">
|
||||||
|
群体规模:
|
||||||
|
<span className="text-[#cf1322]">
|
||||||
|
{item.total}
|
||||||
|
</span>
|
||||||
|
</span>
|
||||||
|
<span className="text-[#909090]">
|
||||||
|
对应实体数:
|
||||||
|
<span className="text-[#cf1322]">
|
||||||
|
{item.real_total}
|
||||||
|
</span>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<span className="text-[#909090]">
|
||||||
|
涉及平台:
|
||||||
|
<span className="text-[#222222]">
|
||||||
|
{item.platforms}
|
||||||
|
</span>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex justify-between mb-[5px]">
|
<div className="absolute top-[5px] right-[10px]">
|
||||||
<span className="text-[#909090]">群体规模:<span className="text-[#cf1322]">{item.total}</span></span>
|
<Checkbox
|
||||||
<span className="text-[#909090]">对应实体数:<span className="text-[#cf1322]">{item.real_total}</span></span>
|
checked={item.checked}
|
||||||
|
onCheckedChange={() =>
|
||||||
|
listDataCheckedChange(index, !item.checked)
|
||||||
|
}
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div><span className="text-[#909090]">涉及平台:<span className="text-[#222222]">{item.platforms}</span></span></div>
|
|
||||||
</div>
|
</div>
|
||||||
<div className="absolute top-[5px] right-[10px]">
|
);
|
||||||
<input type="checkbox" checked={item.checked} onChange={() => listDataCheckedChange(index, !item.checked)} className="w-4 h-4 cursor-pointer accent-primary" />
|
})}
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
))}
|
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
{searchType === "issue" && (
|
{searchType === "issue" && (
|
||||||
<div className="bg-white p-[10px] grid grid-cols-4 gap-[10px]">
|
<div className="bg-white p-[10px] grid grid-cols-4 gap-[10px]">
|
||||||
{listData.map((item, index) => (
|
{listData.map((item: listDataItem, index: number) => {
|
||||||
<div key={index} className="relative shadow-[0px_0px_2px_1px_#ededed] mb-[10px] p-[10px] rounded-[5px]">
|
return (
|
||||||
<div>
|
<div
|
||||||
<div className="truncate mb-[5px]">{item.title}</div>
|
key={index}
|
||||||
<div className="flex justify-between mb-[5px]">
|
className="relative shadow-[0px_0px_2px_1px_#ededed] mb-[10px] p-[10px] rounded-[5px]"
|
||||||
<span className="text-[#909090]">方向:<span className="text-[#222222]">{item.direction}</span></span>
|
>
|
||||||
<span className="text-[#909090]">类别:<span className="text-[#222222]">{item.issue_type}</span></span>
|
<div>
|
||||||
|
<div className="truncate mb-[5px]">
|
||||||
|
{item.title}
|
||||||
|
</div>
|
||||||
|
<div className="flex justify-between mb-[5px]">
|
||||||
|
<span className="text-[#909090]">
|
||||||
|
方向:
|
||||||
|
<span className="text-[#222222]">
|
||||||
|
{item.direction}
|
||||||
|
</span>
|
||||||
|
</span>
|
||||||
|
<span className="text-[#909090]">
|
||||||
|
类别:
|
||||||
|
<span className="text-[#222222]">
|
||||||
|
{item.issue_type}
|
||||||
|
</span>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="absolute top-[5px] right-[10px]">
|
||||||
|
<Checkbox
|
||||||
|
checked={item.checked}
|
||||||
|
onCheckedChange={() =>
|
||||||
|
listDataCheckedChange(index, !item.checked)
|
||||||
|
}
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="absolute top-[5px] right-[10px]">
|
);
|
||||||
<input type="checkbox" checked={item.checked} onChange={() => listDataCheckedChange(index, !item.checked)} className="w-4 h-4 cursor-pointer accent-primary" />
|
})}
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
))}
|
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
{searchType === "event" && (
|
{searchType === "event" && (
|
||||||
<div className="bg-white p-[10px] grid grid-cols-5 gap-[10px]">
|
<div className="bg-white p-[10px] grid grid-cols-5 gap-[10px]">
|
||||||
{listData.map((item, index) => (
|
{listData.map((item: listDataItem, index: number) => {
|
||||||
<div key={index} className="relative shadow-[0px_0px_2px_1px_#ededed] mb-[10px] p-[10px] rounded-[5px]">
|
return (
|
||||||
<div className="flex-1 min-w-0">
|
<div
|
||||||
<p className="truncate hover:underline cursor-pointer" onClick={() => linkToA(item.url)}>{item.title}</p>
|
key={index}
|
||||||
<p className="truncate text-[#909090]">首发新闻:{item.site_name}</p>
|
className="relative shadow-[0px_0px_2px_1px_#ededed] mb-[10px] p-[10px] rounded-[5px]"
|
||||||
<p className="truncate text-[#909090]">事件时间:{item.first_publish_time}</p>
|
>
|
||||||
<p className="truncate text-[#909090]">关键词:{item.keywords?.join("、")}</p>
|
<div className="flex-1 min-w-0">
|
||||||
|
<p
|
||||||
|
className="truncate hover:underline cursor-pointer"
|
||||||
|
onClick={() => linkToA(item.url)}
|
||||||
|
>
|
||||||
|
{item.title}
|
||||||
|
</p>
|
||||||
|
<p className="truncate text-[#909090]">
|
||||||
|
首发新闻:{item.site_name}
|
||||||
|
</p>
|
||||||
|
<p className="truncate text-[#909090]">
|
||||||
|
事件时间:{item.first_publish_time}
|
||||||
|
</p>
|
||||||
|
<p className="truncate text-[#909090]">
|
||||||
|
事件时间:{item.keywords?.join("、")}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<div className="absolute top-[5px] right-[10px]">
|
||||||
|
<Checkbox
|
||||||
|
checked={item.checked}
|
||||||
|
onCheckedChange={() =>
|
||||||
|
listDataCheckedChange(index, !item.checked)
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="absolute top-[5px] right-[10px]">
|
);
|
||||||
<input type="checkbox" checked={item.checked} onChange={() => listDataCheckedChange(index, !item.checked)} className="w-4 h-4 cursor-pointer accent-primary" />
|
})}
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
))}
|
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
{searchType === "threat" && (
|
{searchType === "threat" && (
|
||||||
<div className="bg-white p-[10px] grid grid-cols-5 gap-[10px]">
|
<div className="bg-white p-[10px] grid grid-cols-5 gap-[10px]">
|
||||||
{listData.map((item, index) => (
|
{listData.map((item: listDataItem, index: number) => {
|
||||||
<div key={index} className="relative shadow-[0px_0px_2px_1px_#ededed] mb-[10px] p-[10px] rounded-[5px]">
|
return (
|
||||||
<div className="flex-1 min-w-0 pr-[25px]">
|
<div
|
||||||
<p className="truncate hover:underline cursor-pointer" onClick={() => linkToA(item.url)}>{item.title}</p>
|
key={index}
|
||||||
<p className="truncate text-[#909090]">发布平台:{item.site_name || "暂无数据"}</p>
|
className="relative shadow-[0px_0px_2px_1px_#ededed] mb-[10px] p-[10px] rounded-[5px]"
|
||||||
<p className="truncate text-[#909090]">事件时间:{item.first_publish_time || "暂无数据"}</p>
|
>
|
||||||
<p className="truncate text-[#909090]">关键词:{item.keywords?.join("、") || "暂无数据"}</p>
|
<div className="flex-1 min-w-0 pr-[25px]">
|
||||||
|
<p
|
||||||
|
className="truncate hover:underline cursor-pointer"
|
||||||
|
onClick={() => linkToA(item.url)}
|
||||||
|
>
|
||||||
|
{item.title}
|
||||||
|
</p>
|
||||||
|
<p className="truncate text-[#909090]">
|
||||||
|
发布平台:{item.site_name || "暂无数据"}
|
||||||
|
</p>
|
||||||
|
<p className="truncate text-[#909090]">
|
||||||
|
事件时间:
|
||||||
|
{item.first_publish_time || "暂无数据"}
|
||||||
|
</p>
|
||||||
|
<p className="truncate text-[#909090]">
|
||||||
|
关键词:
|
||||||
|
{item.keywords?.join("、") || "暂无数据"}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<div className="absolute top-[5px] right-[10px]">
|
||||||
|
<Checkbox
|
||||||
|
checked={item.checked}
|
||||||
|
onCheckedChange={() =>
|
||||||
|
listDataCheckedChange(index, !item.checked)
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="absolute top-[5px] right-[10px]">
|
);
|
||||||
<input type="checkbox" checked={item.checked} onChange={() => listDataCheckedChange(index, !item.checked)} className="w-4 h-4 cursor-pointer accent-primary" />
|
})}
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
))}
|
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -0,0 +1,46 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
|
import * as React from "react";
|
||||||
|
import { Check } from "lucide-react";
|
||||||
|
|
||||||
|
import { cn } from "@/lib/utils";
|
||||||
|
|
||||||
|
export interface CheckboxProps
|
||||||
|
extends Omit<
|
||||||
|
React.ButtonHTMLAttributes<HTMLButtonElement>,
|
||||||
|
"onChange" | "checked" | "type"
|
||||||
|
> {
|
||||||
|
checked?: boolean;
|
||||||
|
onCheckedChange?: (checked: boolean) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 轻量级 Checkbox(不依赖 @radix-ui/react-checkbox,适配离线部署)。
|
||||||
|
* API 与 Radix/shadcn 对齐:受控 `checked` + `onCheckedChange(next)`。
|
||||||
|
*/
|
||||||
|
export const Checkbox = React.forwardRef<HTMLButtonElement, CheckboxProps>(
|
||||||
|
({ className, checked = false, onCheckedChange, disabled, ...props }, ref) => {
|
||||||
|
return (
|
||||||
|
<button
|
||||||
|
ref={ref}
|
||||||
|
type="button"
|
||||||
|
role="checkbox"
|
||||||
|
aria-checked={checked}
|
||||||
|
disabled={disabled}
|
||||||
|
data-state={checked ? "checked" : "unchecked"}
|
||||||
|
onClick={() => onCheckedChange?.(!checked)}
|
||||||
|
className={cn(
|
||||||
|
"peer flex size-4 shrink-0 items-center justify-center rounded-[4px] border border-input shadow-sm outline-none transition-colors",
|
||||||
|
"focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2",
|
||||||
|
"disabled:cursor-not-allowed disabled:opacity-50",
|
||||||
|
checked && "border-primary bg-primary text-primary-foreground",
|
||||||
|
className,
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
|
{checked && <Check className="size-3.5" strokeWidth={3} />}
|
||||||
|
</button>
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
Checkbox.displayName = "Checkbox";
|
||||||
@@ -52,7 +52,7 @@ export type MenuItem = {
|
|||||||
export const sidebarMenuItems: MenuItem[] = [
|
export const sidebarMenuItems: MenuItem[] = [
|
||||||
{
|
{
|
||||||
id: "qa-center",
|
id: "qa-center",
|
||||||
label: "问答中心",
|
label: "问答管理",
|
||||||
icon: HelpCircle,
|
icon: HelpCircle,
|
||||||
children: [
|
children: [
|
||||||
{ id: "general-qa", label: "通用问答", icon: CircleHelp, path: "/page/workspace/chats/new" },
|
{ id: "general-qa", label: "通用问答", icon: CircleHelp, path: "/page/workspace/chats/new" },
|
||||||
@@ -65,11 +65,11 @@ export const sidebarMenuItems: MenuItem[] = [
|
|||||||
// { id: "expert-qa", label: "专家问答", icon: Brain, path: "/page/strategy/qa/expert-qa" },
|
// { id: "expert-qa", label: "专家问答", icon: Brain, path: "/page/strategy/qa/expert-qa" },
|
||||||
{
|
{
|
||||||
id: "qa-zzjc",
|
id: "qa-zzjc",
|
||||||
label: "zzJC支持",
|
label: "JC支持",
|
||||||
icon: Bot,
|
icon: Bot,
|
||||||
path: "/page/strategy/qa/zzjc",
|
path: "/page/strategy/qa/zzjc",
|
||||||
},
|
},
|
||||||
{ id: "qa-zzxd", label: "zzXD应用", icon: Layout, path: "/page/strategy/qa/zzxd" },
|
{ id: "qa-zzxd", label: "XD应用", icon: Layout, path: "/page/strategy/qa/zzxd" },
|
||||||
{ id: "qa-research", label: "课题研究", icon: FileText, path: "/page/canvas/ai-writing" },
|
{ id: "qa-research", label: "课题研究", icon: FileText, path: "/page/canvas/ai-writing" },
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
@@ -102,24 +102,24 @@ export const sidebarMenuItems: MenuItem[] = [
|
|||||||
label: "COH中任务",
|
label: "COH中任务",
|
||||||
icon: Activity,
|
icon: Activity,
|
||||||
children: [
|
children: [
|
||||||
{
|
// {
|
||||||
id: "coh-task-3qfx",
|
// id: "coh-task-3qfx",
|
||||||
label: "3QFX",
|
// label: "3QFX",
|
||||||
icon: ListTodo,
|
// icon: ListTodo,
|
||||||
path: "/page/strategy/task-management/plan-task-sentiment",
|
// path: "/page/strategy/task-management/plan-task-sentiment",
|
||||||
},
|
// },
|
||||||
{
|
// {
|
||||||
id: "coh-task-fx",
|
// id: "coh-task-fx",
|
||||||
label: "任务FX",
|
// label: "任务FX",
|
||||||
icon: ListTodo,
|
// icon: ListTodo,
|
||||||
path: "/page/strategy/task-management/plan-task/plan-task-fx",
|
// path: "/page/strategy/task-management/plan-task/plan-task-fx",
|
||||||
},
|
// },
|
||||||
{
|
// {
|
||||||
id: "coh-task-xdfx",
|
// id: "coh-task-xdfx",
|
||||||
label: "XDFX",
|
// label: "XDFX",
|
||||||
icon: ListTodo,
|
// icon: ListTodo,
|
||||||
path: "/page/strategy/task-management/plan-task/plan-task-action",
|
// path: "/page/strategy/task-management/plan-task/plan-task-action",
|
||||||
},
|
// },
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -217,6 +217,81 @@ function AIWritingLayout({ hideAdvancedFields, initialNotebookId, initialDocumen
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 写作页主体(侧栏 + 内容区)。放在 Provider 内部,以便读 useAIWriting 状态:
|
||||||
|
* 进入写作(status !== idle 或查看历史)后自动收起左侧研究模板栏,让写作区更宽。
|
||||||
|
*/
|
||||||
|
function AIWritingBody({ hideAdvancedFields, initialNotebookId, initialDocumentIds, studioToken }: { hideAdvancedFields: boolean } & NotebookProps) {
|
||||||
|
const { state } = useAIWriting();
|
||||||
|
const { isHistoryMode } = useAIWritingHistory();
|
||||||
|
|
||||||
|
// 需求 0616 #5:左栏点「笔记本 / 所有空间」→ 在右侧内嵌打开对应页,替换右侧内容
|
||||||
|
// (写作布局不卸载,仅 display:none 隐藏,避免 WritingSetupChat 重挂丢线程)。
|
||||||
|
const [rightView, setRightView] = useState<RightView>(null);
|
||||||
|
const overlayActive = rightView != null;
|
||||||
|
const viewingNotebookId = rightView?.kind === 'notebook' ? rightView.id : null;
|
||||||
|
|
||||||
|
// 进入写作(开始生成 / 查看历史)后收起研究模板侧栏;嵌入态(studioToken)始终不展示。
|
||||||
|
const showSidebar = !studioToken && state.status === 'idle' && !isHistoryMode;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="flex-1 min-h-0 overflow-hidden flex">
|
||||||
|
{/* 需求 0616 #5:课题研究左栏(研究模板 + 笔记本)。进入写作后收起。 */}
|
||||||
|
{showSidebar && (
|
||||||
|
<ResearchSidebar
|
||||||
|
viewingNotebookId={viewingNotebookId}
|
||||||
|
onOpenNotebook={(id) => setRightView({ kind: 'notebook', id })}
|
||||||
|
onOpenAllSpaces={() => setRightView({ kind: 'allSpaces' })}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
{/* 右侧:写作布局常驻(仅 display:none 隐藏,避免 WritingSetupChat 重挂丢线程),
|
||||||
|
查看笔记本 / 所有空间时在其上叠加内嵌页。 */}
|
||||||
|
<div className="relative flex-1 min-h-0 overflow-hidden">
|
||||||
|
<div className={cn('absolute inset-0', overlayActive && 'hidden')}>
|
||||||
|
<AIWritingLayout hideAdvancedFields={hideAdvancedFields} initialNotebookId={initialNotebookId} initialDocumentIds={initialDocumentIds} studioToken={studioToken} />
|
||||||
|
</div>
|
||||||
|
{overlayActive && (
|
||||||
|
<div className="absolute inset-0 flex flex-col bg-background">
|
||||||
|
<div className="flex shrink-0 items-center gap-1 border-b border-border px-4 py-2">
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={() => setRightView(null)}
|
||||||
|
className="flex items-center gap-1.5 rounded-md px-2 py-1 text-sm text-muted-foreground transition-colors hover:bg-muted hover:text-foreground"
|
||||||
|
>
|
||||||
|
<ArrowLeft className="size-4" />
|
||||||
|
返回写作
|
||||||
|
</button>
|
||||||
|
{rightView.kind === 'notebook' && (
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={() => setRightView({ kind: 'allSpaces' })}
|
||||||
|
className="ml-1 rounded-md px-2 py-1 text-sm text-muted-foreground transition-colors hover:bg-muted hover:text-foreground"
|
||||||
|
>
|
||||||
|
所有空间
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
<div className="min-h-0 flex-1 overflow-hidden">
|
||||||
|
{rightView.kind === 'notebook' ? (
|
||||||
|
// 笔记本详情页内部用 ChatBox(useArtifacts) → 需 ChatRuntime 提供 Provider。
|
||||||
|
<ChatRuntime>
|
||||||
|
<StudioNotebookDetailPage key={rightView.id} notebookId={rightView.id} />
|
||||||
|
</ChatRuntime>
|
||||||
|
) : (
|
||||||
|
<div className="h-full overflow-y-auto">
|
||||||
|
<StudioNotebooksPage
|
||||||
|
onOpenNotebook={(id) => setRightView({ kind: 'notebook', id })}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
export default function AIWritingPage({
|
export default function AIWritingPage({
|
||||||
hideAdvancedFields = false,
|
hideAdvancedFields = false,
|
||||||
initialNotebookId,
|
initialNotebookId,
|
||||||
@@ -228,12 +303,6 @@ export default function AIWritingPage({
|
|||||||
initialDocumentIds?: number[];
|
initialDocumentIds?: number[];
|
||||||
studioToken?: string;
|
studioToken?: string;
|
||||||
} = {}) {
|
} = {}) {
|
||||||
// 需求 0616 #5:左栏点「笔记本 / 所有空间」→ 在右侧内嵌打开对应页,替换右侧内容
|
|
||||||
// (写作布局不卸载,仅 display:none 隐藏,避免 WritingSetupChat 重挂丢线程)。
|
|
||||||
const [rightView, setRightView] = useState<RightView>(null);
|
|
||||||
const overlayActive = rightView != null;
|
|
||||||
const viewingNotebookId = rightView?.kind === 'notebook' ? rightView.id : null;
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<CanvasThreadProvider>
|
<CanvasThreadProvider>
|
||||||
<CanvasGraphProvider>
|
<CanvasGraphProvider>
|
||||||
@@ -247,60 +316,12 @@ export default function AIWritingPage({
|
|||||||
>
|
>
|
||||||
<TooltipProvider delayDuration={300}>
|
<TooltipProvider delayDuration={300}>
|
||||||
<div className="h-full flex flex-col overflow-hidden bg-background canvas-writing-bg">
|
<div className="h-full flex flex-col overflow-hidden bg-background canvas-writing-bg">
|
||||||
<div className="flex-1 min-h-0 overflow-hidden flex">
|
<AIWritingBody
|
||||||
{/* 需求 0616 #5:课题研究左栏(研究模板 + 笔记本)。studio 嵌入态(studioToken)隐藏。 */}
|
hideAdvancedFields={hideAdvancedFields}
|
||||||
{!studioToken && (
|
initialNotebookId={initialNotebookId}
|
||||||
<ResearchSidebar
|
initialDocumentIds={initialDocumentIds}
|
||||||
viewingNotebookId={viewingNotebookId}
|
studioToken={studioToken}
|
||||||
onOpenNotebook={(id) => setRightView({ kind: 'notebook', id })}
|
/>
|
||||||
onOpenAllSpaces={() => setRightView({ kind: 'allSpaces' })}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
{/* 右侧:写作布局常驻(仅 display:none 隐藏,避免 WritingSetupChat 重挂丢线程),
|
|
||||||
查看笔记本 / 所有空间时在其上叠加内嵌页。 */}
|
|
||||||
<div className="relative flex-1 min-h-0 overflow-hidden">
|
|
||||||
<div className={cn('absolute inset-0', overlayActive && 'hidden')}>
|
|
||||||
<AIWritingLayout hideAdvancedFields={hideAdvancedFields} initialNotebookId={initialNotebookId} initialDocumentIds={initialDocumentIds} studioToken={studioToken} />
|
|
||||||
</div>
|
|
||||||
{overlayActive && (
|
|
||||||
<div className="absolute inset-0 flex flex-col bg-background">
|
|
||||||
<div className="flex shrink-0 items-center gap-1 border-b border-border px-4 py-2">
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
onClick={() => setRightView(null)}
|
|
||||||
className="flex items-center gap-1.5 rounded-md px-2 py-1 text-sm text-muted-foreground transition-colors hover:bg-muted hover:text-foreground"
|
|
||||||
>
|
|
||||||
<ArrowLeft className="size-4" />
|
|
||||||
返回写作
|
|
||||||
</button>
|
|
||||||
{rightView.kind === 'notebook' && (
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
onClick={() => setRightView({ kind: 'allSpaces' })}
|
|
||||||
className="ml-1 rounded-md px-2 py-1 text-sm text-muted-foreground transition-colors hover:bg-muted hover:text-foreground"
|
|
||||||
>
|
|
||||||
所有空间
|
|
||||||
</button>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
<div className="min-h-0 flex-1 overflow-hidden">
|
|
||||||
{rightView.kind === 'notebook' ? (
|
|
||||||
// 笔记本详情页内部用 ChatBox(useArtifacts) → 需 ChatRuntime 提供 Provider。
|
|
||||||
<ChatRuntime>
|
|
||||||
<StudioNotebookDetailPage key={rightView.id} notebookId={rightView.id} />
|
|
||||||
</ChatRuntime>
|
|
||||||
) : (
|
|
||||||
<div className="h-full overflow-y-auto">
|
|
||||||
<StudioNotebooksPage
|
|
||||||
onOpenNotebook={(id) => setRightView({ kind: 'notebook', id })}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</TooltipProvider>
|
</TooltipProvider>
|
||||||
</WritingFormProvider>
|
</WritingFormProvider>
|
||||||
|
|||||||
@@ -1,10 +1,18 @@
|
|||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
|
import {
|
||||||
|
BookMarkedIcon,
|
||||||
|
BrainCircuitIcon,
|
||||||
|
CheckIcon,
|
||||||
|
FileTextIcon,
|
||||||
|
GraduationCapIcon,
|
||||||
|
} from "lucide-react";
|
||||||
import { useRouter } from "next/navigation";
|
import { useRouter } from "next/navigation";
|
||||||
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
|
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
|
||||||
|
|
||||||
import { type PromptInputMessage } from "@/components/ai-elements/prompt-input";
|
import { type PromptInputMessage } from "@/components/ai-elements/prompt-input";
|
||||||
import { useArtifacts } from "@/components/workspace/artifacts";
|
import { useArtifacts } from "@/components/workspace/artifacts";
|
||||||
|
import { AgentSelector } from "@/components/workspace/agents/agent-selector";
|
||||||
import {
|
import {
|
||||||
ChatBox,
|
ChatBox,
|
||||||
useSpecificChatMode,
|
useSpecificChatMode,
|
||||||
@@ -12,18 +20,23 @@ import {
|
|||||||
} from "@/components/workspace/chats";
|
} from "@/components/workspace/chats";
|
||||||
import {
|
import {
|
||||||
MessageList,
|
MessageList,
|
||||||
MESSAGE_LIST_DEFAULT_PADDING_BOTTOM,
|
|
||||||
MESSAGE_LIST_FOLLOWUPS_EXTRA_PADDING_BOTTOM,
|
|
||||||
} from "@/components/workspace/messages";
|
} from "@/components/workspace/messages";
|
||||||
import { ThreadContext } from "@/components/workspace/messages/context";
|
import { ThreadContext } from "@/components/workspace/messages/context";
|
||||||
import { TodoList } from "@/components/workspace/todo-list";
|
import { TodoList } from "@/components/workspace/todo-list";
|
||||||
|
import { Button } from "@/components/ui/button";
|
||||||
|
import { useAgentSelector } from "@/core/agents";
|
||||||
import { getAccessToken } from "@/core/auth";
|
import { getAccessToken } from "@/core/auth";
|
||||||
import { useI18n } from "@/core/i18n/hooks";
|
import { useI18n } from "@/core/i18n/hooks";
|
||||||
import { useModels } from "@/core/models/hooks";
|
import { useModels } from "@/core/models/hooks";
|
||||||
import { useNotification } from "@/core/notification/hooks";
|
import { useNotification } from "@/core/notification/hooks";
|
||||||
import { useRouteBase } from "@/core/route-base";
|
import { useRouteBase } from "@/core/route-base";
|
||||||
import { useThreadSettings } from "@/core/settings";
|
import {
|
||||||
import { usePromptPrefixSettings } from "@/core/system-settings/hooks";
|
getLastAgentId,
|
||||||
|
isThreadRagModeLocked,
|
||||||
|
saveLastAgentId,
|
||||||
|
useLocalSettings,
|
||||||
|
useThreadSettings,
|
||||||
|
} from "@/core/settings";
|
||||||
import { useThreadStream } from "@/core/threads/hooks";
|
import { useThreadStream } from "@/core/threads/hooks";
|
||||||
import { textOfMessage } from "@/core/threads/utils";
|
import { textOfMessage } from "@/core/threads/utils";
|
||||||
import { env } from "@/env";
|
import { env } from "@/env";
|
||||||
@@ -81,16 +94,16 @@ export default function IframeChatPage() {
|
|||||||
const routeBase = useRouteBase();
|
const routeBase = useRouteBase();
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const { t } = useI18n();
|
const { t } = useI18n();
|
||||||
const [showFollowups, setShowFollowups] = useState(false);
|
|
||||||
const [hasStartedConversation, setHasStartedConversation] = useState(false);
|
const [hasStartedConversation, setHasStartedConversation] = useState(false);
|
||||||
const { threadId, setThreadId, isNewThread, setIsNewThread, isMock } =
|
const { threadId, setThreadId, isNewThread, setIsNewThread, isMock } =
|
||||||
useThreadChat();
|
useThreadChat();
|
||||||
const [settings] = useThreadSettings(threadId);
|
const [settings, setSettings] = useThreadSettings(threadId);
|
||||||
|
const [localSettings, setLocalSettings] = useLocalSettings();
|
||||||
const [mounted, setMounted] = useState(false);
|
const [mounted, setMounted] = useState(false);
|
||||||
const { tokenUsageEnabled } = useModels();
|
const { tokenUsageEnabled } = useModels();
|
||||||
const { data: promptPrefixSettings } = usePromptPrefixSettings();
|
|
||||||
const [promptPrefixOn, setPromptPrefixOn] = useState(false);
|
|
||||||
const [selectedAgentId, setSelectedAgentId] = useState<string | null>(null);
|
const [selectedAgentId, setSelectedAgentId] = useState<string | null>(null);
|
||||||
|
const { agents: selectorAgents, isLoading: selectorLoading } =
|
||||||
|
useAgentSelector();
|
||||||
const pendingRequestIdRef = useRef<string | null>(null);
|
const pendingRequestIdRef = useRef<string | null>(null);
|
||||||
const { selectedArtifact } = useArtifacts();
|
const { selectedArtifact } = useArtifacts();
|
||||||
useSpecificChatMode();
|
useSpecificChatMode();
|
||||||
@@ -124,11 +137,19 @@ export default function IframeChatPage() {
|
|||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (isNewThread) {
|
if (isNewThread) {
|
||||||
setHasStartedConversation(false);
|
setHasStartedConversation(false);
|
||||||
setPromptPrefixOn(false);
|
setSelectedAgentId(getLastAgentId());
|
||||||
setSelectedAgentId(null);
|
|
||||||
}
|
}
|
||||||
}, [isNewThread, threadId]);
|
}, [isNewThread, threadId]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (selectorLoading || !selectedAgentId) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (!selectorAgents.some((agent) => agent.id === selectedAgentId)) {
|
||||||
|
setSelectedAgentId(null);
|
||||||
|
}
|
||||||
|
}, [selectedAgentId, selectorAgents, selectorLoading]);
|
||||||
|
|
||||||
const { showNotification } = useNotification();
|
const { showNotification } = useNotification();
|
||||||
|
|
||||||
const [thread, sendMessage, isUploading] = useThreadStream({
|
const [thread, sendMessage, isUploading] = useThreadStream({
|
||||||
@@ -207,20 +228,12 @@ export default function IframeChatPage() {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const shouldApplyPrefix =
|
|
||||||
isNewThread &&
|
|
||||||
!hasStartedConversation &&
|
|
||||||
promptPrefixOn &&
|
|
||||||
promptPrefixSettings?.enabled === true;
|
|
||||||
setHasStartedConversation(true);
|
setHasStartedConversation(true);
|
||||||
window.dispatchEvent(
|
window.dispatchEvent(
|
||||||
new CustomEvent("workspace:sandbox-resume-autoscroll"),
|
new CustomEvent("workspace:sandbox-resume-autoscroll"),
|
||||||
);
|
);
|
||||||
|
|
||||||
const extraContext: Record<string, unknown> = {};
|
const extraContext: Record<string, unknown> = {};
|
||||||
if (shouldApplyPrefix) {
|
|
||||||
extraContext.prompt_prefix_enabled = true;
|
|
||||||
}
|
|
||||||
if (settings.context.writing_mode && writingArtifactPath) {
|
if (settings.context.writing_mode && writingArtifactPath) {
|
||||||
extraContext.writing_artifact_path = writingArtifactPath;
|
extraContext.writing_artifact_path = writingArtifactPath;
|
||||||
}
|
}
|
||||||
@@ -238,8 +251,6 @@ export default function IframeChatPage() {
|
|||||||
[
|
[
|
||||||
hasStartedConversation,
|
hasStartedConversation,
|
||||||
isNewThread,
|
isNewThread,
|
||||||
promptPrefixOn,
|
|
||||||
promptPrefixSettings,
|
|
||||||
router,
|
router,
|
||||||
selectedAgentId,
|
selectedAgentId,
|
||||||
sendMessage,
|
sendMessage,
|
||||||
@@ -257,6 +268,45 @@ export default function IframeChatPage() {
|
|||||||
[sendMessage, threadId],
|
[sendMessage, threadId],
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const handleAgentChange = useCallback((agentId: string | null) => {
|
||||||
|
setSelectedAgentId(agentId);
|
||||||
|
saveLastAgentId(agentId);
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const ragModeLocked = isThreadRagModeLocked(threadId);
|
||||||
|
const ragModeEnabled =
|
||||||
|
localSettings.display.ragModeEnabled || ragModeLocked;
|
||||||
|
const proModeEnabled = settings.context.mode === "pro";
|
||||||
|
|
||||||
|
const toggleWritingMode = useCallback(() => {
|
||||||
|
setSettings("context", {
|
||||||
|
writing_mode: !settings.context.writing_mode,
|
||||||
|
});
|
||||||
|
}, [setSettings, settings.context.writing_mode]);
|
||||||
|
|
||||||
|
const toggleMemoryInjection = useCallback(() => {
|
||||||
|
setSettings("context", {
|
||||||
|
memory_injection_enabled:
|
||||||
|
!(settings.context.memory_injection_enabled ?? true),
|
||||||
|
});
|
||||||
|
}, [setSettings, settings.context.memory_injection_enabled]);
|
||||||
|
|
||||||
|
const toggleRagMode = useCallback(() => {
|
||||||
|
if (ragModeLocked) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
setLocalSettings("display", {
|
||||||
|
ragModeEnabled: !ragModeEnabled,
|
||||||
|
});
|
||||||
|
}, [ragModeEnabled, ragModeLocked, setLocalSettings]);
|
||||||
|
|
||||||
|
const toggleProMode = useCallback(() => {
|
||||||
|
setSettings("context", {
|
||||||
|
mode: proModeEnabled ? "flash" : "pro",
|
||||||
|
reasoning_effort: proModeEnabled ? "minimal" : "medium",
|
||||||
|
});
|
||||||
|
}, [proModeEnabled, setSettings]);
|
||||||
|
|
||||||
const handleInterrupt = useCallback(
|
const handleInterrupt = useCallback(
|
||||||
async (message: BrainyChatInterruptMessage) => {
|
async (message: BrainyChatInterruptMessage) => {
|
||||||
const targetRequestId = message.payload?.requestId?.trim();
|
const targetRequestId = message.payload?.requestId?.trim();
|
||||||
@@ -318,11 +368,47 @@ export default function IframeChatPage() {
|
|||||||
};
|
};
|
||||||
}, [handleInterrupt, handleSubmit]);
|
}, [handleInterrupt, handleSubmit]);
|
||||||
|
|
||||||
const messageListPaddingBottom = showFollowups
|
|
||||||
? MESSAGE_LIST_DEFAULT_PADDING_BOTTOM +
|
|
||||||
MESSAGE_LIST_FOLLOWUPS_EXTRA_PADDING_BOTTOM
|
|
||||||
: undefined;
|
|
||||||
const isLandingState = isNewThread && !hasStartedConversation;
|
const isLandingState = isNewThread && !hasStartedConversation;
|
||||||
|
const bottomBarHeightClass = "pb-28";
|
||||||
|
const modeButtons = [
|
||||||
|
{
|
||||||
|
key: "writing",
|
||||||
|
label: "写作",
|
||||||
|
icon: FileTextIcon,
|
||||||
|
active: !!settings.context.writing_mode,
|
||||||
|
onClick: toggleWritingMode,
|
||||||
|
activeClassName:
|
||||||
|
"border-amber-300 bg-amber-50 text-amber-700 hover:bg-amber-100",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: "memory",
|
||||||
|
label: "记忆",
|
||||||
|
icon: BrainCircuitIcon,
|
||||||
|
active: settings.context.memory_injection_enabled ?? true,
|
||||||
|
onClick: toggleMemoryInjection,
|
||||||
|
activeClassName:
|
||||||
|
"border-blue-300 bg-blue-50 text-blue-700 hover:bg-blue-100",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: "references",
|
||||||
|
label: "参考文献",
|
||||||
|
icon: BookMarkedIcon,
|
||||||
|
active: ragModeEnabled,
|
||||||
|
onClick: toggleRagMode,
|
||||||
|
disabled: ragModeLocked,
|
||||||
|
activeClassName:
|
||||||
|
"border-emerald-300 bg-emerald-50 text-emerald-700 hover:bg-emerald-100",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: "pro",
|
||||||
|
label: "专业问答",
|
||||||
|
icon: GraduationCapIcon,
|
||||||
|
active: proModeEnabled,
|
||||||
|
onClick: toggleProMode,
|
||||||
|
activeClassName:
|
||||||
|
"border-violet-300 bg-violet-50 text-violet-700 hover:bg-violet-100",
|
||||||
|
},
|
||||||
|
] as const;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<ThreadContext.Provider value={{ thread, isMock }}>
|
<ThreadContext.Provider value={{ thread, isMock }}>
|
||||||
@@ -350,24 +436,30 @@ export default function IframeChatPage() {
|
|||||||
</header>
|
</header>
|
||||||
*/}
|
*/}
|
||||||
<main className="flex min-h-0 max-w-full grow flex-col overflow-hidden">
|
<main className="flex min-h-0 max-w-full grow flex-col overflow-hidden">
|
||||||
<div className="flex size-full min-w-0">
|
<div className={cn("flex size-full min-w-0", !isLandingState && bottomBarHeightClass)}>
|
||||||
{!isLandingState && (
|
{!isLandingState && (
|
||||||
<MessageList
|
<MessageList
|
||||||
className="size-full"
|
className="size-full"
|
||||||
contentClassName="mx-0 max-w-full px-3 sm:px-4"
|
contentClassName="mx-0 max-w-full px-3 sm:px-4"
|
||||||
threadId={threadId}
|
threadId={threadId}
|
||||||
thread={thread}
|
thread={thread}
|
||||||
paddingBottom={messageListPaddingBottom}
|
paddingBottom={96}
|
||||||
tokenUsageEnabled={tokenUsageEnabled}
|
tokenUsageEnabled={tokenUsageEnabled}
|
||||||
onClarificationSubmit={handleClarificationSubmit}
|
onClarificationSubmit={handleClarificationSubmit}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
<div className="absolute right-0 bottom-0 left-0 z-30 flex justify-center px-4">
|
<div
|
||||||
|
className={cn(
|
||||||
|
"absolute inset-x-0 z-30 flex justify-center px-4",
|
||||||
|
isLandingState
|
||||||
|
? "top-0 bottom-24 items-center"
|
||||||
|
: "bottom-24",
|
||||||
|
)}
|
||||||
|
>
|
||||||
<div
|
<div
|
||||||
className={cn(
|
className={cn(
|
||||||
"relative w-full",
|
"relative w-full",
|
||||||
isLandingState && "-translate-y-[calc(50vh-96px)]",
|
|
||||||
isLandingState
|
isLandingState
|
||||||
? "max-w-(--container-width-md)"
|
? "max-w-(--container-width-md)"
|
||||||
: "max-w-full",
|
: "max-w-full",
|
||||||
@@ -387,6 +479,13 @@ export default function IframeChatPage() {
|
|||||||
{isLandingState && mounted && (
|
{isLandingState && mounted && (
|
||||||
<div className="mb-6 flex min-h-40 flex-col items-center justify-center">
|
<div className="mb-6 flex min-h-40 flex-col items-center justify-center">
|
||||||
<div className="text-2xl font-medium">你好</div>
|
<div className="text-2xl font-medium">你好</div>
|
||||||
|
<div className="mt-4">
|
||||||
|
<AgentSelector
|
||||||
|
selectedAgentId={selectedAgentId}
|
||||||
|
onAgentChange={handleAgentChange}
|
||||||
|
disabled={isUploading}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
{env.NEXT_PUBLIC_STATIC_WEBSITE_ONLY === "true" && (
|
{env.NEXT_PUBLIC_STATIC_WEBSITE_ONLY === "true" && (
|
||||||
@@ -396,6 +495,31 @@ export default function IframeChatPage() {
|
|||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<div className="absolute right-0 bottom-0 left-0 z-40 border-t bg-background/95 backdrop-blur">
|
||||||
|
<div className="mx-auto flex max-w-(--container-width-lg) flex-wrap items-center justify-center gap-2 px-4 py-4">
|
||||||
|
{modeButtons.map((button) => {
|
||||||
|
const Icon = button.icon;
|
||||||
|
return (
|
||||||
|
<Button
|
||||||
|
key={button.key}
|
||||||
|
type="button"
|
||||||
|
variant="outline"
|
||||||
|
disabled={button.disabled || isUploading}
|
||||||
|
onClick={button.onClick}
|
||||||
|
className={cn(
|
||||||
|
"gap-1.5 rounded-full border border-transparent bg-muted/40 text-muted-foreground shadow-none hover:bg-muted hover:text-foreground",
|
||||||
|
button.active && button.activeClassName,
|
||||||
|
button.disabled && "cursor-not-allowed opacity-80",
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
{button.active && <CheckIcon className="size-3.5" />}
|
||||||
|
<Icon className="size-3.5" />
|
||||||
|
<span className="text-xs font-normal">{button.label}</span>
|
||||||
|
</Button>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</main>
|
</main>
|
||||||
</div>
|
</div>
|
||||||
</ChatBox>
|
</ChatBox>
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ import type { LightApp, LightAppRouteParam } from "@/strategy-components/api/lig
|
|||||||
/** iframe 可挂载的父菜单(取自 core/page-layout/sidebar-menu.ts 的现有菜单 id)。 */
|
/** iframe 可挂载的父菜单(取自 core/page-layout/sidebar-menu.ts 的现有菜单 id)。 */
|
||||||
export const MOUNT_OPTIONS: { label: string; value: string }[] = [
|
export const MOUNT_OPTIONS: { label: string; value: string }[] = [
|
||||||
{ label: "XK中任务", value: "xk-center-task" },
|
{ label: "XK中任务", value: "xk-center-task" },
|
||||||
|
{ label: "COH中任务", value: "coh-center-task" },
|
||||||
{ label: "PG中任务", value: "pg-center-task" },
|
{ label: "PG中任务", value: "pg-center-task" },
|
||||||
{ label: "TY中任务", value: "ty-center-task" },
|
{ label: "TY中任务", value: "ty-center-task" },
|
||||||
{ label: "资源管理", value: "resource-management" },
|
{ label: "资源管理", value: "resource-management" },
|
||||||
|
|||||||
@@ -1016,3 +1016,51 @@ html.lightblue .workspace-assistant-card::before {
|
|||||||
html.lightblue .tiptap-writing-editor .ProseMirror ::selection {
|
html.lightblue .tiptap-writing-editor .ProseMirror ::selection {
|
||||||
background: color-mix(in srgb, #55a6fe 35%, transparent);
|
background: color-mix(in srgb, #55a6fe 35%, transparent);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* ── 来源上传弹窗:A系统基因库的分段单选条(custom-radio) ─────────────── */
|
||||||
|
/* 外层容器,模拟 nz-radio-group */
|
||||||
|
.custom-radio-group {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
font-size: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 单个按钮样式 */
|
||||||
|
.custom-radio-btn {
|
||||||
|
position: relative;
|
||||||
|
display: inline-block;
|
||||||
|
padding: 5px 16px;
|
||||||
|
line-height: 12px;
|
||||||
|
background: #fff;
|
||||||
|
border: 1px solid #d9d9d9;
|
||||||
|
cursor: pointer;
|
||||||
|
/* 除第一个外向左移 1px,实现边框重叠,消除双层边框 */
|
||||||
|
margin-left: -1px;
|
||||||
|
transition: all 0.1s ease;
|
||||||
|
user-select: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 第一个按钮:左侧圆角 */
|
||||||
|
.custom-radio-btn:first-child {
|
||||||
|
margin-left: 0;
|
||||||
|
border-radius: 4px 0 0 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 最后一个按钮:右侧圆角 */
|
||||||
|
.custom-radio-btn:last-child {
|
||||||
|
border-radius: 0 4px 4px 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 悬浮状态 */
|
||||||
|
.custom-radio-btn:hover {
|
||||||
|
color: #1890ff;
|
||||||
|
z-index: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 选中状态 */
|
||||||
|
.custom-radio-btn.active {
|
||||||
|
background-color: #e6f7ff;
|
||||||
|
border-color: #1890ff;
|
||||||
|
color: #1890ff;
|
||||||
|
z-index: 2;
|
||||||
|
}
|
||||||
Reference in new issue
Block a user