feat(desktop): 工业化升级 B —— 真桌面集成(Wails Go 桥 + 原生能力)
让它真正像桌面 App 而非套壳网页:启用闲置的 Wails Go 桥,接原生文件框/系统打开/ 通知/无边框标题栏,且全部对浏览器预览(make web)优雅降级。 - app.go:SaveReportAs(原生"另存为"框 + 下载落盘)、OpenReport(下到临时目录 + 系统默认应用打开 docx)、Notify(macOS osascript 通知)、download/openInSystem 跨平台 - main.go:macOS TitleBarHiddenInset —— 隐藏标题栏、内容铺满到顶、保留红绿灯交通灯 - lib/desktop.ts:window.go.main.App 运行时桥 + isDesktop/isMacDesktop 探测; saveReportAs/openReport/notify 在无 Wails(浏览器)时分别降级为 <a download>/新标签/Toast - ReportView:桌面端「另存为 Word」(原生框) +「用系统打开」+ 完成弹系统通知; 浏览器端保持「下载 Word」 - KbView:拖拽文件入库(HTML5 dataTransfer,两种模式通用)+ 拖拽高亮 +「选择文件」按钮 - TopBar:顶栏设为 Wails 可拖拽区(--wails-draggable),控件标 no-drag; macOS 桌面端左留白让位交通灯 验证:GOWORK=off wails build 打出 .app(绑定生成 + mac 标题栏);启动真实原生窗口 截图确认无边框标题栏 + 交通灯内嵌 + 顶栏可拖拽(见会话截图);浏览器(Preview)确认 window.go 不存在时降级正确(下载链路 + 拖拽占位)。tsc + vite build 通过。 Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -32,6 +32,7 @@ export function KbView() {
|
||||
const [hits, setHits] = useState<KbHit[] | null>(null);
|
||||
const [searching, setSearching] = useState(false);
|
||||
const [graph, setGraph] = useState<Triple[] | null>(null);
|
||||
const [dragOver, setDragOver] = useState(false);
|
||||
|
||||
const onGraph = async () => {
|
||||
try {
|
||||
@@ -118,7 +119,27 @@ export function KbView() {
|
||||
{/* 左:入库 + 实时监控 */}
|
||||
<section className="flex w-1/2 flex-col border-r border-line p-4">
|
||||
<h3 className="mb-2 text-xs font-semibold text-slate-400">入库</h3>
|
||||
<Textarea className="h-24 resize-none" value={text} onChange={(e) => setText(e.target.value)} placeholder="每行一条知识,或上传文件" />
|
||||
<div
|
||||
onDragOver={(e) => {
|
||||
e.preventDefault();
|
||||
setDragOver(true);
|
||||
}}
|
||||
onDragLeave={() => setDragOver(false)}
|
||||
onDrop={(e) => {
|
||||
e.preventDefault();
|
||||
setDragOver(false);
|
||||
const f = e.dataTransfer.files?.[0];
|
||||
if (f) onFile(f);
|
||||
}}
|
||||
className={cn("relative rounded-md", dragOver && "ring-2 ring-brand")}
|
||||
>
|
||||
<Textarea className="h-24 w-full resize-none" value={text} onChange={(e) => setText(e.target.value)} placeholder="每行一条知识,或把文件拖到这里 / 点选择文件" />
|
||||
{dragOver && (
|
||||
<div className="pointer-events-none absolute inset-0 flex items-center justify-center rounded-md bg-ink-950/85 text-xs font-medium text-brand-400">
|
||||
松手以入库该文件
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<div className="mt-2 flex items-center gap-2">
|
||||
<Button variant="primary" size="sm" icon={Upload} onClick={onIngest} disabled={ingesting || !text.trim()}>
|
||||
{ingesting ? "入库中…" : "入库文本"}
|
||||
|
||||
@@ -1,9 +1,16 @@
|
||||
import { useRef, useState } from "react";
|
||||
import { Play, Download, FileText } from "lucide-react";
|
||||
import { Play, Download, FileText, ExternalLink } from "lucide-react";
|
||||
import { generateReport, streamTokens, streamExec, reportDownloadUrl, type Identity, type ExecEvent } from "../lib/api";
|
||||
import { isDesktop, saveReportAs, openReport, notify } from "../lib/desktop";
|
||||
import { ExecTrace } from "../components/ExecTrace";
|
||||
import { Button, Input, Field, Panel, Dot, EmptyState, useToast } from "../ui";
|
||||
|
||||
// 安全文件名:去掉路径不安全字符,限长。
|
||||
function reportFilename(topic: string, id: string): string {
|
||||
const base = topic.replace(/[\\/:*?"<>|]/g, "").trim().slice(0, 40) || id;
|
||||
return `${base}.docx`;
|
||||
}
|
||||
|
||||
type Phase = "idle" | "running" | "done" | "error";
|
||||
|
||||
// 报告生成:输入主题(+可选知识库) → 触发后端专用编排
|
||||
@@ -43,7 +50,8 @@ export function ReportView({ identity }: { identity: Identity }) {
|
||||
(tok) => setOut((o) => o + tok),
|
||||
() => {
|
||||
setPhase("done");
|
||||
toast.push("success", "报告已生成,可下载 Word");
|
||||
toast.push("success", "报告已生成,可保存 Word");
|
||||
notify("报告已生成", topic.trim());
|
||||
},
|
||||
() => {
|
||||
setPhase("error");
|
||||
@@ -83,13 +91,26 @@ export function ReportView({ identity }: { identity: Identity }) {
|
||||
{running ? "生成中…" : "生成报告"}
|
||||
</Button>
|
||||
{phase === "done" && taskId ? (
|
||||
<a
|
||||
href={reportDownloadUrl(taskId)}
|
||||
className="inline-flex h-9 items-center gap-1.5 rounded-md border border-brand/60 px-4 text-sm font-medium text-brand-400 transition hover:bg-brand/10"
|
||||
>
|
||||
<Download className="h-4 w-4" />
|
||||
下载 Word
|
||||
</a>
|
||||
<div className="flex items-center gap-2">
|
||||
<Button
|
||||
icon={Download}
|
||||
onClick={async () => {
|
||||
const p = await saveReportAs(reportDownloadUrl(taskId), reportFilename(topic, taskId));
|
||||
if (p) toast.push("success", "已保存到 " + p);
|
||||
}}
|
||||
>
|
||||
{isDesktop() ? "另存为 Word" : "下载 Word"}
|
||||
</Button>
|
||||
{isDesktop() && (
|
||||
<Button
|
||||
variant="ghost"
|
||||
icon={ExternalLink}
|
||||
onClick={() => openReport(reportDownloadUrl(taskId), reportFilename(topic, taskId)).catch((e) => toast.push("error", String(e)))}
|
||||
>
|
||||
用系统打开
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
) : (
|
||||
<span className="h-9" />
|
||||
)}
|
||||
|
||||
Reference in New Issue
Block a user