feat(frontend): 对共享文件应用自动三方合并
This commit is contained in:
parent
0d255e9ac4
commit
e6183e84fc
File diff suppressed because it is too large
Load Diff
|
|
@ -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) {
|
||||
|
|
|
|||
|
|
@ -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";
|
||||
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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)}
|
||||
|
|
|
|||
|
|
@ -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];
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in New Issue