| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182 |
- "use client";
- import { useState, useEffect } from "react";
- import { useRouter } from "next/navigation";
- import { ResponsiveLayout } from "@/components/layout";
- import { Button } from "@/components/ui/button";
- import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
- import { fetchPrompts, updatePrompt, type PromptItem } from "@/features/agent/services/promptsApi";
- import { toast } from "@/hooks/useToast";
- import type { I18nKey } from "@/lib/i18n/dict";
- import { useI18n } from "@/lib/i18n/provider";
- const PROMPT_HINT_KEYS: Partial<Record<string, I18nKey>> = {
- rag_prompt: "agent.prompts.hint.rag_prompt",
- rag_prompt_with_web_optional: "agent.prompts.hint.rag_prompt_with_web_optional",
- no_kb_prompt: "agent.prompts.hint.no_kb_prompt",
- web_search_result_prompt: "agent.prompts.hint.web_search_result_prompt",
- no_source_reply: "agent.prompts.hint.no_source_reply",
- ai_fail_reply: "agent.prompts.hint.ai_fail_reply",
- };
- const PROMPT_USAGE_KEYS: Partial<Record<string, I18nKey>> = {
- rag_prompt: "agent.prompts.usage.rag_prompt",
- rag_prompt_with_web_optional: "agent.prompts.usage.rag_prompt_with_web_optional",
- no_kb_prompt: "agent.prompts.usage.no_kb_prompt",
- web_search_result_prompt: "agent.prompts.usage.web_search_result_prompt",
- no_source_reply: "agent.prompts.usage.no_source_reply",
- ai_fail_reply: "agent.prompts.usage.ai_fail_reply",
- };
- function getTextareaMinHeight(key: string): string {
- return key === "no_source_reply" || key === "ai_fail_reply" ? "min-h-[80px]" : "min-h-[200px]";
- }
- export default function PromptsPage({ embedded = false }: { embedded?: boolean }) {
- const router = useRouter();
- const { t } = useI18n();
- const [userId, setUserId] = useState<number | null>(null);
- const [prompts, setPrompts] = useState<PromptItem[]>([]);
- const [loading, setLoading] = useState(true);
- const [savingKey, setSavingKey] = useState<string | null>(null);
- const [error, setError] = useState("");
- useEffect(() => {
- const storedUserId = localStorage.getItem("agent_user_id");
- if (!storedUserId) {
- router.push("/");
- return;
- }
- setUserId(Number.parseInt(storedUserId, 10));
- }, [router]);
- const loadPrompts = async () => {
- if (!userId) return;
- try {
- setLoading(true);
- setError("");
- const data = await fetchPrompts(userId);
- setPrompts(data);
- } catch (e) {
- console.error("加载提示词失败:", e);
- setError((e as Error).message || t("agent.prompts.loadFailed"));
- } finally {
- setLoading(false);
- }
- };
- useEffect(() => {
- if (userId) loadPrompts();
- }, [userId]);
- const handleSave = async (key: string, content: string) => {
- if (!userId) return;
- setSavingKey(key);
- try {
- await updatePrompt(userId, key, content);
- toast.success(t("agent.prompts.saveSuccess"));
- await loadPrompts();
- } catch (e) {
- toast.error((e as Error).message || t("agent.prompts.saveFailed"));
- } finally {
- setSavingKey(null);
- }
- };
- const handleContentChange = (key: string, content: string) => {
- setPrompts((prev) =>
- prev.map((p) => (p.key === key ? { ...p, content } : p))
- );
- };
- if (!userId) return null;
- const headerContent = (
- <div className="border-b bg-card p-3 shadow-sm sm:p-4">
- <div className="flex flex-col sm:flex-row justify-between items-start sm:items-center gap-4">
- <div>
- <h1 className="text-xl font-bold text-foreground">{t("agent.prompts.title")}</h1>
- <div className="text-sm text-muted-foreground mt-1">
- {t("agent.prompts.subtitle")}
- </div>
- </div>
- {!embedded && (
- <Button
- onClick={() => router.push("/agent/dashboard")}
- variant="outline"
- size="sm"
- >
- {t("agent.settings.backDashboard")}
- </Button>
- )}
- </div>
- </div>
- );
- const mainContent = (
- <div className="flex-1 overflow-auto p-3 sm:p-4 md:p-6">
- <div className="max-w-4xl mx-auto space-y-6">
- {error && (
- <div className="p-3 bg-red-50 border border-red-200 rounded-md text-red-600 text-sm">
- {error}
- </div>
- )}
- {loading ? (
- <div className="text-center py-12 text-muted-foreground">{t("common.loading")}</div>
- ) : (
- prompts.map((item) => {
- const usageKey = PROMPT_USAGE_KEYS[item.key];
- const hintKey = PROMPT_HINT_KEYS[item.key] ?? "agent.prompts.hint.default";
- return (
- <Card key={item.key}>
- <CardHeader>
- <CardTitle className="text-base">{item.name}</CardTitle>
- {usageKey && (
- <p className="text-sm text-muted-foreground mt-1">
- <span className="font-medium">{t("agent.prompts.usageLabel")}</span>
- {t(usageKey)}
- </p>
- )}
- <p className="text-xs text-muted-foreground mt-1">{t(hintKey)}</p>
- </CardHeader>
- <CardContent className="space-y-3">
- <textarea
- className={`w-full ${getTextareaMinHeight(item.key)} px-3 py-2 border border-input rounded-md text-sm bg-background font-mono resize-y`}
- value={item.content}
- onChange={(e) => handleContentChange(item.key, e.target.value)}
- placeholder={
- item.key === "no_source_reply" || item.key === "ai_fail_reply"
- ? t("agent.prompts.ph.shortReply")
- : t("agent.prompts.ph.withPlaceholders")
- }
- spellCheck={false}
- />
- <Button
- size="sm"
- onClick={() => handleSave(item.key, item.content)}
- disabled={savingKey === item.key}
- >
- {savingKey === item.key ? t("agent.prompts.saving") : t("agent.prompts.save")}
- </Button>
- </CardContent>
- </Card>
- );
- })
- )}
- </div>
- </div>
- );
- if (embedded) {
- return (
- <div className="flex-1 flex flex-col min-h-0 overflow-hidden">
- {headerContent}
- {mainContent}
- </div>
- );
- }
- return (
- <ResponsiveLayout header={headerContent} main={mainContent} />
- );
- }
|