page.tsx 6.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182
  1. "use client";
  2. import { useState, useEffect } from "react";
  3. import { useRouter } from "next/navigation";
  4. import { ResponsiveLayout } from "@/components/layout";
  5. import { Button } from "@/components/ui/button";
  6. import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
  7. import { fetchPrompts, updatePrompt, type PromptItem } from "@/features/agent/services/promptsApi";
  8. import { toast } from "@/hooks/useToast";
  9. import type { I18nKey } from "@/lib/i18n/dict";
  10. import { useI18n } from "@/lib/i18n/provider";
  11. const PROMPT_HINT_KEYS: Partial<Record<string, I18nKey>> = {
  12. rag_prompt: "agent.prompts.hint.rag_prompt",
  13. rag_prompt_with_web_optional: "agent.prompts.hint.rag_prompt_with_web_optional",
  14. no_kb_prompt: "agent.prompts.hint.no_kb_prompt",
  15. web_search_result_prompt: "agent.prompts.hint.web_search_result_prompt",
  16. no_source_reply: "agent.prompts.hint.no_source_reply",
  17. ai_fail_reply: "agent.prompts.hint.ai_fail_reply",
  18. };
  19. const PROMPT_USAGE_KEYS: Partial<Record<string, I18nKey>> = {
  20. rag_prompt: "agent.prompts.usage.rag_prompt",
  21. rag_prompt_with_web_optional: "agent.prompts.usage.rag_prompt_with_web_optional",
  22. no_kb_prompt: "agent.prompts.usage.no_kb_prompt",
  23. web_search_result_prompt: "agent.prompts.usage.web_search_result_prompt",
  24. no_source_reply: "agent.prompts.usage.no_source_reply",
  25. ai_fail_reply: "agent.prompts.usage.ai_fail_reply",
  26. };
  27. function getTextareaMinHeight(key: string): string {
  28. return key === "no_source_reply" || key === "ai_fail_reply" ? "min-h-[80px]" : "min-h-[200px]";
  29. }
  30. export default function PromptsPage({ embedded = false }: { embedded?: boolean }) {
  31. const router = useRouter();
  32. const { t } = useI18n();
  33. const [userId, setUserId] = useState<number | null>(null);
  34. const [prompts, setPrompts] = useState<PromptItem[]>([]);
  35. const [loading, setLoading] = useState(true);
  36. const [savingKey, setSavingKey] = useState<string | null>(null);
  37. const [error, setError] = useState("");
  38. useEffect(() => {
  39. const storedUserId = localStorage.getItem("agent_user_id");
  40. if (!storedUserId) {
  41. router.push("/");
  42. return;
  43. }
  44. setUserId(Number.parseInt(storedUserId, 10));
  45. }, [router]);
  46. const loadPrompts = async () => {
  47. if (!userId) return;
  48. try {
  49. setLoading(true);
  50. setError("");
  51. const data = await fetchPrompts(userId);
  52. setPrompts(data);
  53. } catch (e) {
  54. console.error("加载提示词失败:", e);
  55. setError((e as Error).message || t("agent.prompts.loadFailed"));
  56. } finally {
  57. setLoading(false);
  58. }
  59. };
  60. useEffect(() => {
  61. if (userId) loadPrompts();
  62. }, [userId]);
  63. const handleSave = async (key: string, content: string) => {
  64. if (!userId) return;
  65. setSavingKey(key);
  66. try {
  67. await updatePrompt(userId, key, content);
  68. toast.success(t("agent.prompts.saveSuccess"));
  69. await loadPrompts();
  70. } catch (e) {
  71. toast.error((e as Error).message || t("agent.prompts.saveFailed"));
  72. } finally {
  73. setSavingKey(null);
  74. }
  75. };
  76. const handleContentChange = (key: string, content: string) => {
  77. setPrompts((prev) =>
  78. prev.map((p) => (p.key === key ? { ...p, content } : p))
  79. );
  80. };
  81. if (!userId) return null;
  82. const headerContent = (
  83. <div className="border-b bg-card p-3 shadow-sm sm:p-4">
  84. <div className="flex flex-col sm:flex-row justify-between items-start sm:items-center gap-4">
  85. <div>
  86. <h1 className="text-xl font-bold text-foreground">{t("agent.prompts.title")}</h1>
  87. <div className="text-sm text-muted-foreground mt-1">
  88. {t("agent.prompts.subtitle")}
  89. </div>
  90. </div>
  91. {!embedded && (
  92. <Button
  93. onClick={() => router.push("/agent/dashboard")}
  94. variant="outline"
  95. size="sm"
  96. >
  97. {t("agent.settings.backDashboard")}
  98. </Button>
  99. )}
  100. </div>
  101. </div>
  102. );
  103. const mainContent = (
  104. <div className="flex-1 overflow-auto p-3 sm:p-4 md:p-6">
  105. <div className="max-w-4xl mx-auto space-y-6">
  106. {error && (
  107. <div className="p-3 bg-red-50 border border-red-200 rounded-md text-red-600 text-sm">
  108. {error}
  109. </div>
  110. )}
  111. {loading ? (
  112. <div className="text-center py-12 text-muted-foreground">{t("common.loading")}</div>
  113. ) : (
  114. prompts.map((item) => {
  115. const usageKey = PROMPT_USAGE_KEYS[item.key];
  116. const hintKey = PROMPT_HINT_KEYS[item.key] ?? "agent.prompts.hint.default";
  117. return (
  118. <Card key={item.key}>
  119. <CardHeader>
  120. <CardTitle className="text-base">{item.name}</CardTitle>
  121. {usageKey && (
  122. <p className="text-sm text-muted-foreground mt-1">
  123. <span className="font-medium">{t("agent.prompts.usageLabel")}</span>
  124. {t(usageKey)}
  125. </p>
  126. )}
  127. <p className="text-xs text-muted-foreground mt-1">{t(hintKey)}</p>
  128. </CardHeader>
  129. <CardContent className="space-y-3">
  130. <textarea
  131. className={`w-full ${getTextareaMinHeight(item.key)} px-3 py-2 border border-input rounded-md text-sm bg-background font-mono resize-y`}
  132. value={item.content}
  133. onChange={(e) => handleContentChange(item.key, e.target.value)}
  134. placeholder={
  135. item.key === "no_source_reply" || item.key === "ai_fail_reply"
  136. ? t("agent.prompts.ph.shortReply")
  137. : t("agent.prompts.ph.withPlaceholders")
  138. }
  139. spellCheck={false}
  140. />
  141. <Button
  142. size="sm"
  143. onClick={() => handleSave(item.key, item.content)}
  144. disabled={savingKey === item.key}
  145. >
  146. {savingKey === item.key ? t("agent.prompts.saving") : t("agent.prompts.save")}
  147. </Button>
  148. </CardContent>
  149. </Card>
  150. );
  151. })
  152. )}
  153. </div>
  154. </div>
  155. );
  156. if (embedded) {
  157. return (
  158. <div className="flex-1 flex flex-col min-h-0 overflow-hidden">
  159. {headerContent}
  160. {mainContent}
  161. </div>
  162. );
  163. }
  164. return (
  165. <ResponsiveLayout header={headerContent} main={mainContent} />
  166. );
  167. }