Feat: 取消自动断线,回到首页后能查看项目的连线情况
This commit is contained in:
parent
ff1cb22475
commit
735b37a617
|
|
@ -16,6 +16,7 @@
|
||||||
"@radix-ui/react-dropdown-menu": "^2.1.16",
|
"@radix-ui/react-dropdown-menu": "^2.1.16",
|
||||||
"@radix-ui/react-separator": "^1.1.8",
|
"@radix-ui/react-separator": "^1.1.8",
|
||||||
"@radix-ui/react-slot": "^1.2.3",
|
"@radix-ui/react-slot": "^1.2.3",
|
||||||
|
"@radix-ui/react-tooltip": "^1.2.8",
|
||||||
"@tailwindcss/vite": "^4.1.16",
|
"@tailwindcss/vite": "^4.1.16",
|
||||||
"class-variance-authority": "^0.7.1",
|
"class-variance-authority": "^0.7.1",
|
||||||
"clsx": "^2.1.1",
|
"clsx": "^2.1.1",
|
||||||
|
|
|
||||||
|
|
@ -23,6 +23,9 @@ importers:
|
||||||
'@radix-ui/react-slot':
|
'@radix-ui/react-slot':
|
||||||
specifier: ^1.2.3
|
specifier: ^1.2.3
|
||||||
version: 1.2.3(@types/react@19.2.2)(react@19.2.0)
|
version: 1.2.3(@types/react@19.2.2)(react@19.2.0)
|
||||||
|
'@radix-ui/react-tooltip':
|
||||||
|
specifier: ^1.2.8
|
||||||
|
version: 1.2.8(@types/react-dom@19.2.2(@types/react@19.2.2))(@types/react@19.2.2)(react-dom@19.2.0(react@19.2.0))(react@19.2.0)
|
||||||
'@tailwindcss/vite':
|
'@tailwindcss/vite':
|
||||||
specifier: ^4.1.16
|
specifier: ^4.1.16
|
||||||
version: 4.1.16(vite@7.1.12(@types/node@24.9.2)(jiti@2.6.1)(lightningcss@1.30.2))
|
version: 4.1.16(vite@7.1.12(@types/node@24.9.2)(jiti@2.6.1)(lightningcss@1.30.2))
|
||||||
|
|
@ -702,6 +705,19 @@ packages:
|
||||||
'@types/react':
|
'@types/react':
|
||||||
optional: true
|
optional: true
|
||||||
|
|
||||||
|
'@radix-ui/react-tooltip@1.2.8':
|
||||||
|
resolution: {integrity: sha512-tY7sVt1yL9ozIxvmbtN5qtmH2krXcBCfjEiCgKGLqunJHvgvZG2Pcl2oQ3kbcZARb1BGEHdkLzcYGO8ynVlieg==}
|
||||||
|
peerDependencies:
|
||||||
|
'@types/react': '*'
|
||||||
|
'@types/react-dom': '*'
|
||||||
|
react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
|
||||||
|
react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
|
||||||
|
peerDependenciesMeta:
|
||||||
|
'@types/react':
|
||||||
|
optional: true
|
||||||
|
'@types/react-dom':
|
||||||
|
optional: true
|
||||||
|
|
||||||
'@radix-ui/react-use-callback-ref@1.1.1':
|
'@radix-ui/react-use-callback-ref@1.1.1':
|
||||||
resolution: {integrity: sha512-FkBMwD+qbGQeMu1cOHnuGB6x4yzPjho8ap5WtbEJ26umhgqVXbhekKUQO+hZEL1vU92a3wHwdp0HAcqAUF5iDg==}
|
resolution: {integrity: sha512-FkBMwD+qbGQeMu1cOHnuGB6x4yzPjho8ap5WtbEJ26umhgqVXbhekKUQO+hZEL1vU92a3wHwdp0HAcqAUF5iDg==}
|
||||||
peerDependencies:
|
peerDependencies:
|
||||||
|
|
@ -774,6 +790,19 @@ packages:
|
||||||
'@types/react':
|
'@types/react':
|
||||||
optional: true
|
optional: true
|
||||||
|
|
||||||
|
'@radix-ui/react-visually-hidden@1.2.3':
|
||||||
|
resolution: {integrity: sha512-pzJq12tEaaIhqjbzpCuv/OypJY/BPavOofm+dbab+MHLajy277+1lLm6JFcGgF5eskJ6mquGirhXY2GD/8u8Ug==}
|
||||||
|
peerDependencies:
|
||||||
|
'@types/react': '*'
|
||||||
|
'@types/react-dom': '*'
|
||||||
|
react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
|
||||||
|
react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
|
||||||
|
peerDependenciesMeta:
|
||||||
|
'@types/react':
|
||||||
|
optional: true
|
||||||
|
'@types/react-dom':
|
||||||
|
optional: true
|
||||||
|
|
||||||
'@radix-ui/rect@1.1.1':
|
'@radix-ui/rect@1.1.1':
|
||||||
resolution: {integrity: sha512-HPwpGIzkl28mWyZqG52jiqDJ12waP11Pa1lGoiyUkIEuMLBP0oeK/C89esbXrxsky5we7dfd8U58nm0SgAWpVw==}
|
resolution: {integrity: sha512-HPwpGIzkl28mWyZqG52jiqDJ12waP11Pa1lGoiyUkIEuMLBP0oeK/C89esbXrxsky5we7dfd8U58nm0SgAWpVw==}
|
||||||
|
|
||||||
|
|
@ -2390,6 +2419,26 @@ snapshots:
|
||||||
optionalDependencies:
|
optionalDependencies:
|
||||||
'@types/react': 19.2.2
|
'@types/react': 19.2.2
|
||||||
|
|
||||||
|
'@radix-ui/react-tooltip@1.2.8(@types/react-dom@19.2.2(@types/react@19.2.2))(@types/react@19.2.2)(react-dom@19.2.0(react@19.2.0))(react@19.2.0)':
|
||||||
|
dependencies:
|
||||||
|
'@radix-ui/primitive': 1.1.3
|
||||||
|
'@radix-ui/react-compose-refs': 1.1.2(@types/react@19.2.2)(react@19.2.0)
|
||||||
|
'@radix-ui/react-context': 1.1.2(@types/react@19.2.2)(react@19.2.0)
|
||||||
|
'@radix-ui/react-dismissable-layer': 1.1.11(@types/react-dom@19.2.2(@types/react@19.2.2))(@types/react@19.2.2)(react-dom@19.2.0(react@19.2.0))(react@19.2.0)
|
||||||
|
'@radix-ui/react-id': 1.1.1(@types/react@19.2.2)(react@19.2.0)
|
||||||
|
'@radix-ui/react-popper': 1.2.8(@types/react-dom@19.2.2(@types/react@19.2.2))(@types/react@19.2.2)(react-dom@19.2.0(react@19.2.0))(react@19.2.0)
|
||||||
|
'@radix-ui/react-portal': 1.1.9(@types/react-dom@19.2.2(@types/react@19.2.2))(@types/react@19.2.2)(react-dom@19.2.0(react@19.2.0))(react@19.2.0)
|
||||||
|
'@radix-ui/react-presence': 1.1.5(@types/react-dom@19.2.2(@types/react@19.2.2))(@types/react@19.2.2)(react-dom@19.2.0(react@19.2.0))(react@19.2.0)
|
||||||
|
'@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.2.2(@types/react@19.2.2))(@types/react@19.2.2)(react-dom@19.2.0(react@19.2.0))(react@19.2.0)
|
||||||
|
'@radix-ui/react-slot': 1.2.3(@types/react@19.2.2)(react@19.2.0)
|
||||||
|
'@radix-ui/react-use-controllable-state': 1.2.2(@types/react@19.2.2)(react@19.2.0)
|
||||||
|
'@radix-ui/react-visually-hidden': 1.2.3(@types/react-dom@19.2.2(@types/react@19.2.2))(@types/react@19.2.2)(react-dom@19.2.0(react@19.2.0))(react@19.2.0)
|
||||||
|
react: 19.2.0
|
||||||
|
react-dom: 19.2.0(react@19.2.0)
|
||||||
|
optionalDependencies:
|
||||||
|
'@types/react': 19.2.2
|
||||||
|
'@types/react-dom': 19.2.2(@types/react@19.2.2)
|
||||||
|
|
||||||
'@radix-ui/react-use-callback-ref@1.1.1(@types/react@19.2.2)(react@19.2.0)':
|
'@radix-ui/react-use-callback-ref@1.1.1(@types/react@19.2.2)(react@19.2.0)':
|
||||||
dependencies:
|
dependencies:
|
||||||
react: 19.2.0
|
react: 19.2.0
|
||||||
|
|
@ -2444,6 +2493,15 @@ snapshots:
|
||||||
optionalDependencies:
|
optionalDependencies:
|
||||||
'@types/react': 19.2.2
|
'@types/react': 19.2.2
|
||||||
|
|
||||||
|
'@radix-ui/react-visually-hidden@1.2.3(@types/react-dom@19.2.2(@types/react@19.2.2))(@types/react@19.2.2)(react-dom@19.2.0(react@19.2.0))(react@19.2.0)':
|
||||||
|
dependencies:
|
||||||
|
'@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.2.2(@types/react@19.2.2))(@types/react@19.2.2)(react-dom@19.2.0(react@19.2.0))(react@19.2.0)
|
||||||
|
react: 19.2.0
|
||||||
|
react-dom: 19.2.0(react@19.2.0)
|
||||||
|
optionalDependencies:
|
||||||
|
'@types/react': 19.2.2
|
||||||
|
'@types/react-dom': 19.2.2(@types/react@19.2.2)
|
||||||
|
|
||||||
'@radix-ui/rect@1.1.1': {}
|
'@radix-ui/rect@1.1.1': {}
|
||||||
|
|
||||||
'@rolldown/pluginutils@1.0.0-beta.43': {}
|
'@rolldown/pluginutils@1.0.0-beta.43': {}
|
||||||
|
|
|
||||||
|
|
@ -1,54 +1,75 @@
|
||||||
import { disconnectLanguage, useFileConnections } from "@/store/file-connection";
|
import {
|
||||||
|
disconnectLanguage,
|
||||||
|
useFileConnections,
|
||||||
|
} from "@/store/file-connection";
|
||||||
|
import { Tooltip, TooltipContent, TooltipTrigger } from "../ui/tooltip";
|
||||||
|
import { Button } from "../ui/button";
|
||||||
|
import { cn } from "@/lib/utils";
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
projectId: string;
|
projectId: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
export function HeaderConnectionIndicator({ projectId }: Props) {
|
export function HeaderConnectionIndicator({ projectId }: Props) {
|
||||||
const snap = useFileConnections(projectId);
|
const snap = useFileConnections(projectId);
|
||||||
const list = Object.values(snap.connections);
|
const list = Object.values(snap.connections);
|
||||||
const hasAny = list.length > 0;
|
const hasAny = list.length > 0;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="relative group">
|
<Tooltip>
|
||||||
<button
|
<TooltipTrigger asChild>
|
||||||
type="button"
|
<Button
|
||||||
className={`px-2 h-8 rounded border text-sm bg-white ${hasAny ? "text-foreground" : "text-muted-foreground"}`}
|
variant="outline"
|
||||||
title={hasAny ? "已连线文件" : "未连线到任何文件"}
|
title={hasAny ? "已连线文件" : "未连线到任何文件"}
|
||||||
>
|
>
|
||||||
{hasAny ? `已连线 ${list.length}` : "未连线"}
|
<span
|
||||||
</button>
|
className={cn(
|
||||||
<div className="invisible opacity-0 group-hover:visible group-hover:opacity-100 transition-opacity duration-150 absolute right-0 top-full mt-2 z-50 w-80 rounded-md border bg-white text-foreground shadow">
|
"size-2 rounded-full",
|
||||||
<div className="p-3">
|
hasAny ? "bg-green-500" : "bg-red-500"
|
||||||
<div className="text-sm font-medium mb-2">连线状态</div>
|
)}
|
||||||
{list.length === 0 ? (
|
></span>
|
||||||
<div className="text-sm text-muted-foreground">暂无连线。通过“导入 JSON”选择文件后将建立连线。</div>
|
{hasAny ? `已连线 ${list.length}` : "未连线"}
|
||||||
) : (
|
</Button>
|
||||||
<div className="space-y-2">
|
</TooltipTrigger>
|
||||||
{list.map((c) => (
|
<TooltipContent className="p-4">
|
||||||
<div key={c.language} className="flex items-start justify-between gap-2">
|
<div className="text-sm font-medium mb-2">连线状态</div>
|
||||||
<div className="min-w-0">
|
{list.length === 0 ? (
|
||||||
<div className="text-sm font-medium truncate">{c.language}</div>
|
<div className="text-sm text-muted-foreground">
|
||||||
<div className="text-xs text-muted-foreground truncate" title={c.name}>
|
暂无连线。通过“导入 JSON”选择文件后将建立连线。
|
||||||
{c.name}
|
</div>
|
||||||
</div>
|
) : (
|
||||||
</div>
|
<div className="space-y-2">
|
||||||
<button
|
{list.map((c) => (
|
||||||
type="button"
|
<div
|
||||||
className="px-2 h-7 rounded border text-xs hover:bg-accent"
|
key={c.language}
|
||||||
onClick={() => disconnectLanguage(projectId, c.language)}
|
className="flex items-start justify-between gap-2"
|
||||||
>
|
>
|
||||||
断开
|
<div className="min-w-0">
|
||||||
</button>
|
<div className="text-sm font-medium truncate">
|
||||||
</div>
|
{c.language}
|
||||||
))}
|
</div>
|
||||||
<div className="text-xs text-muted-foreground">
|
<div
|
||||||
注:出于隐私,浏览器不提供完整路径,仅显示文件名;刷新页面后连线不会自动恢复。
|
className="text-xs text-muted-foreground truncate"
|
||||||
</div>
|
title={c.name}
|
||||||
</div>
|
>
|
||||||
)}
|
{c.name}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
<button
|
||||||
);
|
type="button"
|
||||||
|
className="px-2 h-7 rounded border text-xs hover:bg-accent"
|
||||||
|
onClick={() => disconnectLanguage(projectId, c.language)}
|
||||||
|
>
|
||||||
|
断开
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
<div className="text-xs text-muted-foreground">
|
||||||
|
注:出于隐私,浏览器不提供完整路径,仅显示文件名;刷新页面后连线不会自动恢复。
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</TooltipContent>
|
||||||
|
</Tooltip>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,59 @@
|
||||||
|
import * as React from "react"
|
||||||
|
import * as TooltipPrimitive from "@radix-ui/react-tooltip"
|
||||||
|
|
||||||
|
import { cn } from "@/lib/utils"
|
||||||
|
|
||||||
|
function TooltipProvider({
|
||||||
|
delayDuration = 0,
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<typeof TooltipPrimitive.Provider>) {
|
||||||
|
return (
|
||||||
|
<TooltipPrimitive.Provider
|
||||||
|
data-slot="tooltip-provider"
|
||||||
|
delayDuration={delayDuration}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function Tooltip({
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<typeof TooltipPrimitive.Root>) {
|
||||||
|
return (
|
||||||
|
<TooltipProvider>
|
||||||
|
<TooltipPrimitive.Root data-slot="tooltip" {...props} />
|
||||||
|
</TooltipProvider>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function TooltipTrigger({
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<typeof TooltipPrimitive.Trigger>) {
|
||||||
|
return <TooltipPrimitive.Trigger data-slot="tooltip-trigger" {...props} />
|
||||||
|
}
|
||||||
|
|
||||||
|
function TooltipContent({
|
||||||
|
className,
|
||||||
|
sideOffset = 0,
|
||||||
|
children,
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<typeof TooltipPrimitive.Content>) {
|
||||||
|
return (
|
||||||
|
<TooltipPrimitive.Portal>
|
||||||
|
<TooltipPrimitive.Content
|
||||||
|
data-slot="tooltip-content"
|
||||||
|
sideOffset={sideOffset}
|
||||||
|
className={cn(
|
||||||
|
"bg-foreground text-background animate-in fade-in-0 zoom-in-95 data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=closed]:zoom-out-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 z-50 w-fit origin-(--radix-tooltip-content-transform-origin) rounded-md px-3 py-1.5 text-xs text-balance",
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
<TooltipPrimitive.Arrow className="bg-foreground fill-foreground z-50 size-2.5 translate-y-[calc(-50%_-_2px)] rotate-45 rounded-[2px]" />
|
||||||
|
</TooltipPrimitive.Content>
|
||||||
|
</TooltipPrimitive.Portal>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export { Tooltip, TooltipTrigger, TooltipContent, TooltipProvider }
|
||||||
|
|
@ -38,7 +38,7 @@ import { useClipboard } from "@/hooks/use-clipboard";
|
||||||
import { toast } from "sonner";
|
import { toast } from "sonner";
|
||||||
import { Checkbox } from "@/components/ui/checkbox";
|
import { Checkbox } from "@/components/ui/checkbox";
|
||||||
import { Textarea } from "@/components/ui/textarea";
|
import { Textarea } from "@/components/ui/textarea";
|
||||||
import { clearAllConnections, useFileConnections, writeLanguageToConnectedFile } from "@/store/file-connection";
|
import { useFileConnections, writeLanguageToConnectedFile } from "@/store/file-connection";
|
||||||
import { generateLanguageJson } from "@/lib/utils";
|
import { generateLanguageJson } from "@/lib/utils";
|
||||||
import { HeaderConnectionIndicator } from "@/components/biz/header-connection-indicator";
|
import { HeaderConnectionIndicator } from "@/components/biz/header-connection-indicator";
|
||||||
import { SyncFromFilesButton } from "@/components/biz/sync-from-files-button";
|
import { SyncFromFilesButton } from "@/components/biz/sync-from-files-button";
|
||||||
|
|
@ -457,14 +457,6 @@ export default function Editor() {
|
||||||
};
|
};
|
||||||
}, [projectId, languages]);
|
}, [projectId, languages]);
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
return () => {
|
|
||||||
if (projectId) {
|
|
||||||
clearAllConnections(projectId);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
}, [projectId]);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="h-screen flex flex-col">
|
<div className="h-screen flex flex-col">
|
||||||
<header className="h-14 px-2 md:px-4 from-blue-400 to-cyan-400 bg-linear-to-r text-white">
|
<header className="h-14 px-2 md:px-4 from-blue-400 to-cyan-400 bg-linear-to-r text-white">
|
||||||
|
|
|
||||||
|
|
@ -5,6 +5,8 @@ import { Button } from "@/components/ui/button";
|
||||||
import { createProject, deleteProjectDeep, listProjects, type Project, updateProject } from "@/lib/db";
|
import { createProject, deleteProjectDeep, listProjects, type Project, updateProject } from "@/lib/db";
|
||||||
import { ProjectSettingsModal } from "@/components/biz/project-settings-modal";
|
import { ProjectSettingsModal } from "@/components/biz/project-settings-modal";
|
||||||
import { Settings } from "lucide-react";
|
import { Settings } from "lucide-react";
|
||||||
|
import { useConnectionFlags } from "@/store/file-connection";
|
||||||
|
import { cn } from "@/lib/utils";
|
||||||
|
|
||||||
function formatTime(ts: number) {
|
function formatTime(ts: number) {
|
||||||
try {
|
try {
|
||||||
|
|
@ -23,6 +25,7 @@ function App() {
|
||||||
const [settingsOpen, setSettingsOpen] = useState(false);
|
const [settingsOpen, setSettingsOpen] = useState(false);
|
||||||
const [currentProject, setCurrentProject] = useState<Project | null>(null);
|
const [currentProject, setCurrentProject] = useState<Project | null>(null);
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
|
const flags = useConnectionFlags();
|
||||||
|
|
||||||
async function refresh() {
|
async function refresh() {
|
||||||
setLoading(true);
|
setLoading(true);
|
||||||
|
|
@ -88,39 +91,52 @@ function App() {
|
||||||
<div className="text-sm text-muted-foreground">暂无项目,创建一个开始吧。</div>
|
<div className="text-sm text-muted-foreground">暂无项目,创建一个开始吧。</div>
|
||||||
) : (
|
) : (
|
||||||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
|
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
|
||||||
{projects.map((p) => (
|
{projects.map((p) => {
|
||||||
<div
|
const hasConn = !!flags[p.id];
|
||||||
key={p.id}
|
|
||||||
className="rounded-md border p-4 hover:shadow-sm transition cursor-pointer relative"
|
return (
|
||||||
onClick={() => navigate(`/editor/${p.id}`)}
|
<div
|
||||||
role="button"
|
key={p.id}
|
||||||
tabIndex={0}
|
className="rounded-md border p-4 hover:shadow-sm transition cursor-pointer relative"
|
||||||
onKeyDown={(e) => {
|
onClick={() => navigate(`/editor/${p.id}`)}
|
||||||
if (e.key === "Enter" || e.key === " ") navigate(`/editor/${p.id}`);
|
role="button"
|
||||||
}}
|
tabIndex={0}
|
||||||
>
|
onKeyDown={(e) => {
|
||||||
<button
|
if (e.key === "Enter" || e.key === " ") navigate(`/editor/${p.id}`);
|
||||||
type="button"
|
|
||||||
aria-label="项目设置"
|
|
||||||
className="absolute top-2 right-2 inline-flex items-center justify-center rounded-md border px-2 py-1 text-xs text-muted-foreground hover:bg-accent"
|
|
||||||
onClick={(e) => {
|
|
||||||
e.stopPropagation();
|
|
||||||
setCurrentProject(p);
|
|
||||||
setSettingsOpen(true);
|
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Settings className="size-4" />
|
<button
|
||||||
</button>
|
type="button"
|
||||||
<div className="truncate font-medium pr-8">{p.name}</div>
|
aria-label="项目设置"
|
||||||
<div className="mt-1 text-xs text-muted-foreground truncate">ID: {p.id}</div>
|
className="absolute top-2 right-2 inline-flex items-center justify-center rounded-md border px-2 py-1 text-xs text-muted-foreground hover:bg-accent"
|
||||||
<div className="mt-1 text-xs text-muted-foreground">创建时间: {formatTime(p.createdAt)}</div>
|
onClick={(e) => {
|
||||||
{p.preferences?.aiPrompt ? (
|
e.stopPropagation();
|
||||||
<div className="mt-3 text-xs line-clamp-3 text-muted-foreground">
|
setCurrentProject(p);
|
||||||
偏好:{p.preferences.aiPrompt}
|
setSettingsOpen(true);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Settings className="size-4" />
|
||||||
|
</button>
|
||||||
|
<div className="truncate font-medium pr-8 flex items-center gap-2">
|
||||||
|
<span
|
||||||
|
className={cn(
|
||||||
|
"inline-block size-2 rounded-full",
|
||||||
|
hasConn ? "bg-green-500" : "bg-zinc-300"
|
||||||
|
)}
|
||||||
|
title={hasConn ? "已连线" : "未连线"}
|
||||||
|
/>
|
||||||
|
{p.name}
|
||||||
</div>
|
</div>
|
||||||
) : null}
|
<div className="mt-1 text-xs text-muted-foreground truncate">ID: {p.id}</div>
|
||||||
</div>
|
<div className="mt-1 text-xs text-muted-foreground">创建时间: {formatTime(p.createdAt)}</div>
|
||||||
))}
|
{p.preferences?.aiPrompt ? (
|
||||||
|
<div className="mt-3 text-xs line-clamp-3 text-muted-foreground">
|
||||||
|
偏好:{p.preferences.aiPrompt}
|
||||||
|
</div>
|
||||||
|
) : null}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
})}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -16,6 +16,10 @@ type Listener = () => void;
|
||||||
// projectId -> Snapshot
|
// projectId -> Snapshot
|
||||||
const state = new Map<string, Snapshot>();
|
const state = new Map<string, Snapshot>();
|
||||||
const listeners = new Map<string, Set<Listener>>();
|
const listeners = new Map<string, Set<Listener>>();
|
||||||
|
const globalListeners = new Set<Listener>();
|
||||||
|
let globalVersion = 0;
|
||||||
|
let cachedFlags: Record<string, boolean> | null = null;
|
||||||
|
let cachedFlagsVersion = -1;
|
||||||
|
|
||||||
function ensureProject(projectId: string) {
|
function ensureProject(projectId: string) {
|
||||||
if (!state.has(projectId)) {
|
if (!state.has(projectId)) {
|
||||||
|
|
@ -28,8 +32,18 @@ function ensureProject(projectId: string) {
|
||||||
|
|
||||||
function emit(projectId: string) {
|
function emit(projectId: string) {
|
||||||
const set = listeners.get(projectId);
|
const set = listeners.get(projectId);
|
||||||
if (!set) return;
|
if (set) {
|
||||||
for (const l of Array.from(set)) {
|
for (const l of Array.from(set)) {
|
||||||
|
try {
|
||||||
|
l();
|
||||||
|
} catch {
|
||||||
|
// ignore
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// bump global version and invalidate cache
|
||||||
|
globalVersion += 1;
|
||||||
|
for (const l of Array.from(globalListeners)) {
|
||||||
try {
|
try {
|
||||||
l();
|
l();
|
||||||
} catch {
|
} catch {
|
||||||
|
|
@ -56,6 +70,28 @@ export function isFilePickerSupported(): boolean {
|
||||||
return typeof window !== "undefined" && typeof (window as unknown as { showOpenFilePicker?: unknown }).showOpenFilePicker === "function";
|
return typeof window !== "undefined" && typeof (window as unknown as { showOpenFilePicker?: unknown }).showOpenFilePicker === "function";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function useConnectionFlags(): Record<string, boolean> {
|
||||||
|
return useSyncExternalStore(
|
||||||
|
(l) => {
|
||||||
|
globalListeners.add(l);
|
||||||
|
return () => globalListeners.delete(l);
|
||||||
|
},
|
||||||
|
() => {
|
||||||
|
if (cachedFlags && cachedFlagsVersion === globalVersion) {
|
||||||
|
return cachedFlags;
|
||||||
|
}
|
||||||
|
const flags: Record<string, boolean> = {};
|
||||||
|
for (const [pid, snap] of state.entries()) {
|
||||||
|
flags[pid] = Object.keys(snap.connections).length > 0;
|
||||||
|
}
|
||||||
|
cachedFlags = flags;
|
||||||
|
cachedFlagsVersion = globalVersion;
|
||||||
|
return cachedFlags;
|
||||||
|
},
|
||||||
|
() => ({})
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
export async function connectLanguageToFile(projectId: string, language: string): Promise<{ text: string; connection: LanguageConnection } | null> {
|
export async function connectLanguageToFile(projectId: string, language: string): Promise<{ text: string; connection: LanguageConnection } | null> {
|
||||||
if (!isFilePickerSupported()) {
|
if (!isFilePickerSupported()) {
|
||||||
toast.error("当前浏览器不支持文件系统访问 API(showOpenFilePicker)");
|
toast.error("当前浏览器不支持文件系统访问 API(showOpenFilePicker)");
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue