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,
|
null,
|
||||||
);
|
);
|
||||||
const recognitionRef = useRef<SpeechRecognition | null>(null);
|
const recognitionRef = useRef<SpeechRecognition | null>(null);
|
||||||
|
const callbacksRef = useRef({ textareaRef, onTranscriptionChange });
|
||||||
|
callbacksRef.current = { textareaRef, onTranscriptionChange };
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (
|
if (
|
||||||
|
|
@ -1229,15 +1231,18 @@ export const PromptInputSpeechButton = ({
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (finalTranscript && textareaRef?.current) {
|
const currentTextareaRef = callbacksRef.current.textareaRef;
|
||||||
const textarea = textareaRef.current;
|
const currentOnTranscriptionChange = callbacksRef.current.onTranscriptionChange;
|
||||||
|
|
||||||
|
if (finalTranscript && currentTextareaRef?.current) {
|
||||||
|
const textarea = currentTextareaRef.current;
|
||||||
const currentValue = textarea.value;
|
const currentValue = textarea.value;
|
||||||
const newValue =
|
const newValue =
|
||||||
currentValue + (currentValue ? " " : "") + finalTranscript;
|
currentValue + (currentValue ? " " : "") + finalTranscript;
|
||||||
|
|
||||||
textarea.value = newValue;
|
textarea.value = newValue;
|
||||||
textarea.dispatchEvent(new Event("input", { bubbles: true }));
|
textarea.dispatchEvent(new Event("input", { bubbles: true }));
|
||||||
onTranscriptionChange?.(newValue);
|
currentOnTranscriptionChange?.(newValue);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
@ -1255,7 +1260,7 @@ export const PromptInputSpeechButton = ({
|
||||||
recognitionRef.current.stop();
|
recognitionRef.current.stop();
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
}, [textareaRef, onTranscriptionChange]);
|
}, []);
|
||||||
|
|
||||||
const toggleListening = useCallback(() => {
|
const toggleListening = useCallback(() => {
|
||||||
if (!recognition) {
|
if (!recognition) {
|
||||||
|
|
|
||||||
|
|
@ -3,7 +3,6 @@
|
||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
import { ScrollArea, ScrollBar } from "@/components/ui/scroll-area";
|
import { ScrollArea, ScrollBar } from "@/components/ui/scroll-area";
|
||||||
import { cn } from "@/lib/utils";
|
import { cn } from "@/lib/utils";
|
||||||
import { Icon } from "@radix-ui/react-select";
|
|
||||||
import type { LucideIcon } from "lucide-react";
|
import type { LucideIcon } from "lucide-react";
|
||||||
import { Children, type ComponentProps } from "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 { useSidebar } from "@/components/ui/sidebar";
|
||||||
import { env } from "@/env";
|
import { env } from "@/env";
|
||||||
|
|
@ -39,20 +45,24 @@ export function ArtifactsProvider({ children }: ArtifactsProviderProps) {
|
||||||
const [fullscreen, setFullscreen] = useState(false);
|
const [fullscreen, setFullscreen] = useState(false);
|
||||||
const { setOpen: setSidebarOpen } = useSidebar();
|
const { setOpen: setSidebarOpen } = useSidebar();
|
||||||
|
|
||||||
const select = (artifact: string, autoSelect = false) => {
|
const select = useCallback(
|
||||||
setSelectedArtifact(artifact);
|
(artifact: string, autoSelect = false) => {
|
||||||
if (env.NEXT_PUBLIC_STATIC_WEBSITE_ONLY !== "true") {
|
setSelectedArtifact(artifact);
|
||||||
setSidebarOpen(false);
|
if (env.NEXT_PUBLIC_STATIC_WEBSITE_ONLY !== "true") {
|
||||||
}
|
setSidebarOpen(false);
|
||||||
if (!autoSelect) {
|
}
|
||||||
setAutoSelect(false);
|
if (!autoSelect) {
|
||||||
}
|
setAutoSelect(false);
|
||||||
};
|
}
|
||||||
|
},
|
||||||
|
[setSidebarOpen, setSelectedArtifact, setAutoSelect],
|
||||||
|
);
|
||||||
|
|
||||||
const deselect = () => {
|
const deselect = useCallback(() => {
|
||||||
setSelectedArtifact(null);
|
setSelectedArtifact(null);
|
||||||
setAutoSelect(true);
|
setAutoSelect(true);
|
||||||
};
|
setOpen(false);
|
||||||
|
}, []);
|
||||||
|
|
||||||
const value: ArtifactsContextType = {
|
const value: ArtifactsContextType = {
|
||||||
artifacts,
|
artifacts,
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,14 @@
|
||||||
"use client";
|
"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 Link from "next/link";
|
||||||
import { useParams, usePathname, useRouter } from "next/navigation";
|
import { useParams, usePathname, useRouter } from "next/navigation";
|
||||||
import { useCallback, useState } from "react";
|
import { useCallback, useState } from "react";
|
||||||
|
|
@ -19,6 +27,9 @@ import {
|
||||||
DropdownMenuContent,
|
DropdownMenuContent,
|
||||||
DropdownMenuItem,
|
DropdownMenuItem,
|
||||||
DropdownMenuSeparator,
|
DropdownMenuSeparator,
|
||||||
|
DropdownMenuSub,
|
||||||
|
DropdownMenuSubContent,
|
||||||
|
DropdownMenuSubTrigger,
|
||||||
DropdownMenuTrigger,
|
DropdownMenuTrigger,
|
||||||
} from "@/components/ui/dropdown-menu";
|
} from "@/components/ui/dropdown-menu";
|
||||||
import { Input } from "@/components/ui/input";
|
import { Input } from "@/components/ui/input";
|
||||||
|
|
@ -31,12 +42,18 @@ import {
|
||||||
SidebarMenuButton,
|
SidebarMenuButton,
|
||||||
SidebarMenuItem,
|
SidebarMenuItem,
|
||||||
} from "@/components/ui/sidebar";
|
} from "@/components/ui/sidebar";
|
||||||
|
import { getAPIClient } from "@/core/api";
|
||||||
import { useI18n } from "@/core/i18n/hooks";
|
import { useI18n } from "@/core/i18n/hooks";
|
||||||
|
import {
|
||||||
|
exportThreadAsJSON,
|
||||||
|
exportThreadAsMarkdown,
|
||||||
|
} from "@/core/threads/export";
|
||||||
import {
|
import {
|
||||||
useDeleteThread,
|
useDeleteThread,
|
||||||
useRenameThread,
|
useRenameThread,
|
||||||
useThreads,
|
useThreads,
|
||||||
} from "@/core/threads/hooks";
|
} from "@/core/threads/hooks";
|
||||||
|
import type { AgentThread, AgentThreadState } from "@/core/threads/types";
|
||||||
import { pathOfThread, titleOfThread } from "@/core/threads/utils";
|
import { pathOfThread, titleOfThread } from "@/core/threads/utils";
|
||||||
import { env } from "@/env";
|
import { env } from "@/env";
|
||||||
import { copyToClipboard } from "@/lib/utils";
|
import { copyToClipboard } from "@/lib/utils";
|
||||||
|
|
@ -111,6 +128,32 @@ export function RecentChatList() {
|
||||||
},
|
},
|
||||||
[t],
|
[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) {
|
if (threads.length === 0) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
@ -173,6 +216,30 @@ export function RecentChatList() {
|
||||||
<Share2 className="text-muted-foreground" />
|
<Share2 className="text-muted-foreground" />
|
||||||
<span>{t.common.share}</span>
|
<span>{t.common.share}</span>
|
||||||
</DropdownMenuItem>
|
</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 />
|
<DropdownMenuSeparator />
|
||||||
<DropdownMenuItem
|
<DropdownMenuItem
|
||||||
onSelect={() => handleDelete(thread.thread_id)}
|
onSelect={() => handleDelete(thread.thread_id)}
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,4 @@
|
||||||
import { useCallback, useState } from "react";
|
import { useCallback, useLayoutEffect, useState } from "react";
|
||||||
import { useEffect } from "react";
|
|
||||||
|
|
||||||
import {
|
import {
|
||||||
DEFAULT_LOCAL_SETTINGS,
|
DEFAULT_LOCAL_SETTINGS,
|
||||||
|
|
@ -17,7 +16,7 @@ export function useLocalSettings(): [
|
||||||
] {
|
] {
|
||||||
const [mounted, setMounted] = useState(false);
|
const [mounted, setMounted] = useState(false);
|
||||||
const [state, setState] = useState<LocalSettings>(DEFAULT_LOCAL_SETTINGS);
|
const [state, setState] = useState<LocalSettings>(DEFAULT_LOCAL_SETTINGS);
|
||||||
useEffect(() => {
|
useLayoutEffect(() => {
|
||||||
if (!mounted) {
|
if (!mounted) {
|
||||||
setState(getLocalSettings());
|
setState(getLocalSettings());
|
||||||
}
|
}
|
||||||
|
|
@ -28,6 +27,7 @@ export function useLocalSettings(): [
|
||||||
key: keyof LocalSettings,
|
key: keyof LocalSettings,
|
||||||
value: Partial<LocalSettings[keyof LocalSettings]>,
|
value: Partial<LocalSettings[keyof LocalSettings]>,
|
||||||
) => {
|
) => {
|
||||||
|
if (!mounted) return;
|
||||||
setState((prev) => {
|
setState((prev) => {
|
||||||
const newState = {
|
const newState = {
|
||||||
...prev,
|
...prev,
|
||||||
|
|
@ -41,7 +41,7 @@ export function useLocalSettings(): [
|
||||||
return newState;
|
return newState;
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
[],
|
[mounted],
|
||||||
);
|
);
|
||||||
return [state, setter];
|
return [state, setter];
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue