feat(frontend): 对共享文件应用自动三方合并

This commit is contained in:
肖应宇 2026-03-28 22:43:54 +08:00
parent 0d255e9ac4
commit e6183e84fc
6 changed files with 438 additions and 315 deletions

File diff suppressed because it is too large Load Diff

View File

@ -1197,6 +1197,8 @@ export const PromptInputSpeechButton = ({
null,
);
const recognitionRef = useRef<SpeechRecognition | null>(null);
const callbacksRef = useRef({ textareaRef, onTranscriptionChange });
callbacksRef.current = { textareaRef, onTranscriptionChange };
useEffect(() => {
if (
@ -1229,15 +1231,18 @@ export const PromptInputSpeechButton = ({
}
}
if (finalTranscript && textareaRef?.current) {
const textarea = textareaRef.current;
const currentTextareaRef = callbacksRef.current.textareaRef;
const currentOnTranscriptionChange = callbacksRef.current.onTranscriptionChange;
if (finalTranscript && currentTextareaRef?.current) {
const textarea = currentTextareaRef.current;
const currentValue = textarea.value;
const newValue =
currentValue + (currentValue ? " " : "") + finalTranscript;
textarea.value = newValue;
textarea.dispatchEvent(new Event("input", { bubbles: true }));
onTranscriptionChange?.(newValue);
currentOnTranscriptionChange?.(newValue);
}
};
@ -1255,7 +1260,7 @@ export const PromptInputSpeechButton = ({
recognitionRef.current.stop();
}
};
}, [textareaRef, onTranscriptionChange]);
}, []);
const toggleListening = useCallback(() => {
if (!recognition) {

View File

@ -3,7 +3,6 @@
import { Button } from "@/components/ui/button";
import { ScrollArea, ScrollBar } from "@/components/ui/scroll-area";
import { cn } from "@/lib/utils";
import { Icon } from "@radix-ui/react-select";
import type { LucideIcon } from "lucide-react";
import { Children, type ComponentProps } from "react";

View File

@ -1,4 +1,10 @@
import { createContext, useContext, useState, type ReactNode } from "react";
import {
createContext,
useCallback,
useContext,
useState,
type ReactNode,
} from "react";
import { useSidebar } from "@/components/ui/sidebar";
import { env } from "@/env";
@ -39,20 +45,24 @@ export function ArtifactsProvider({ children }: ArtifactsProviderProps) {
const [fullscreen, setFullscreen] = useState(false);
const { setOpen: setSidebarOpen } = useSidebar();
const select = (artifact: string, autoSelect = false) => {
setSelectedArtifact(artifact);
if (env.NEXT_PUBLIC_STATIC_WEBSITE_ONLY !== "true") {
setSidebarOpen(false);
}
if (!autoSelect) {
setAutoSelect(false);
}
};
const select = useCallback(
(artifact: string, autoSelect = false) => {
setSelectedArtifact(artifact);
if (env.NEXT_PUBLIC_STATIC_WEBSITE_ONLY !== "true") {
setSidebarOpen(false);
}
if (!autoSelect) {
setAutoSelect(false);
}
},
[setSidebarOpen, setSelectedArtifact, setAutoSelect],
);
const deselect = () => {
const deselect = useCallback(() => {
setSelectedArtifact(null);
setAutoSelect(true);
};
setOpen(false);
}, []);
const value: ArtifactsContextType = {
artifacts,

View File

@ -1,6 +1,14 @@
"use client";
import { MoreHorizontal, Pencil, Share2, Trash2 } from "lucide-react";
import {
Download,
FileJson,
FileText,
MoreHorizontal,
Pencil,
Share2,
Trash2,
} from "lucide-react";
import Link from "next/link";
import { useParams, usePathname, useRouter } from "next/navigation";
import { useCallback, useState } from "react";
@ -19,6 +27,9 @@ import {
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuSeparator,
DropdownMenuSub,
DropdownMenuSubContent,
DropdownMenuSubTrigger,
DropdownMenuTrigger,
} from "@/components/ui/dropdown-menu";
import { Input } from "@/components/ui/input";
@ -31,12 +42,18 @@ import {
SidebarMenuButton,
SidebarMenuItem,
} from "@/components/ui/sidebar";
import { getAPIClient } from "@/core/api";
import { useI18n } from "@/core/i18n/hooks";
import {
exportThreadAsJSON,
exportThreadAsMarkdown,
} from "@/core/threads/export";
import {
useDeleteThread,
useRenameThread,
useThreads,
} from "@/core/threads/hooks";
import type { AgentThread, AgentThreadState } from "@/core/threads/types";
import { pathOfThread, titleOfThread } from "@/core/threads/utils";
import { env } from "@/env";
import { copyToClipboard } from "@/lib/utils";
@ -111,6 +128,32 @@ export function RecentChatList() {
},
[t],
);
const handleExport = useCallback(
async (thread: AgentThread, format: "markdown" | "json") => {
try {
const apiClient = getAPIClient();
const state = await apiClient.threads.getState<AgentThreadState>(
thread.thread_id,
);
const messages = state.values?.messages ?? [];
if (messages.length === 0) {
toast.error(t.conversation.noMessages);
return;
}
if (format === "markdown") {
exportThreadAsMarkdown(thread, messages);
} else {
exportThreadAsJSON(thread, messages);
}
toast.success(t.common.exportSuccess);
} catch {
toast.error("Failed to export conversation");
}
},
[t],
);
if (threads.length === 0) {
return null;
}
@ -173,6 +216,30 @@ export function RecentChatList() {
<Share2 className="text-muted-foreground" />
<span>{t.common.share}</span>
</DropdownMenuItem>
<DropdownMenuSub>
<DropdownMenuSubTrigger>
<Download className="text-muted-foreground" />
<span>{t.common.export}</span>
</DropdownMenuSubTrigger>
<DropdownMenuSubContent>
<DropdownMenuItem
onSelect={() =>
handleExport(thread, "markdown")
}
>
<FileText className="text-muted-foreground" />
<span>{t.common.exportAsMarkdown}</span>
</DropdownMenuItem>
<DropdownMenuItem
onSelect={() =>
handleExport(thread, "json")
}
>
<FileJson className="text-muted-foreground" />
<span>{t.common.exportAsJSON}</span>
</DropdownMenuItem>
</DropdownMenuSubContent>
</DropdownMenuSub>
<DropdownMenuSeparator />
<DropdownMenuItem
onSelect={() => handleDelete(thread.thread_id)}

View File

@ -1,5 +1,4 @@
import { useCallback, useState } from "react";
import { useEffect } from "react";
import { useCallback, useLayoutEffect, useState } from "react";
import {
DEFAULT_LOCAL_SETTINGS,
@ -17,7 +16,7 @@ export function useLocalSettings(): [
] {
const [mounted, setMounted] = useState(false);
const [state, setState] = useState<LocalSettings>(DEFAULT_LOCAL_SETTINGS);
useEffect(() => {
useLayoutEffect(() => {
if (!mounted) {
setState(getLocalSettings());
}
@ -28,6 +27,7 @@ export function useLocalSettings(): [
key: keyof LocalSettings,
value: Partial<LocalSettings[keyof LocalSettings]>,
) => {
if (!mounted) return;
setState((prev) => {
const newState = {
...prev,
@ -41,7 +41,7 @@ export function useLocalSettings(): [
return newState;
});
},
[],
[mounted],
);
return [state, setter];
}