diff --git a/apps/fronted/src/app/api/polish/route.ts b/apps/fronted/src/app/api/polish/route.ts new file mode 100644 index 0000000..c0f5d62 --- /dev/null +++ b/apps/fronted/src/app/api/polish/route.ts @@ -0,0 +1,103 @@ +import { NextResponse } from "next/server"; + +export async function POST(req: Request) { + try { + const body = await req.json(); + const { apiKey, model, content } = body; + + const response = await fetch( + "https://ark.cn-beijing.volces.com/api/v3/chat/completions", + { + method: "POST", + headers: { + "Content-Type": "application/json", + Authorization: `Bearer ${apiKey}`, + }, + body: JSON.stringify({ + model, + messages: [ + { + role: "system", + content: `你是一个专业的简历优化助手。请帮助优化以下文本,使其更加专业和有吸引力。 + + 优化原则: + 1. 使用更专业的词汇和表达方式 + 2. 突出关键成就和技能 + 3. 保持简洁清晰 + 4. 使用主动语气 + 5. 保持原有信息的完整性 + 6. 保留我输入的格式 + + 请直接返回优化后的文本,不要包含任何解释或其他内容。`, + }, + { + role: "user", + content, + }, + ], + stream: true, + }), + } + ); + + const encoder = new TextEncoder(); + const stream = new ReadableStream({ + async start(controller) { + if (!response.body) { + controller.close(); + return; + } + + const reader = response.body.getReader(); + const decoder = new TextDecoder(); + + try { + while (true) { + const { done, value } = await reader.read(); + if (done) { + controller.close(); + break; + } + + const chunk = decoder.decode(value); + const lines = chunk + .split("\n") + .filter((line) => line.trim() !== ""); + + for (const line of lines) { + if (line.includes("[DONE]")) continue; + if (!line.startsWith("data:")) continue; + + try { + const data = JSON.parse(line.slice(5)); + const content = data.choices[0]?.delta?.content; + if (content) { + controller.enqueue(encoder.encode(content)); + } + } catch (e) { + console.error("Error parsing JSON:", e); + } + } + } + } catch (error) { + console.error("Stream reading error:", error); + controller.error(error); + } + }, + }); + + return new Response(stream, { + headers: { + "Content-Type": "text/event-stream", + "Cache-Control": "no-cache", + Connection: "keep-alive", + }, + }); + } catch (error) { + console.error("Polish error:", error); + return NextResponse.json( + { error: "Failed to polish content" }, + { status: 500 } + ); + } +} diff --git a/apps/fronted/src/components/editor/Field.tsx b/apps/fronted/src/components/editor/Field.tsx index eee56c6..2ff261c 100644 --- a/apps/fronted/src/components/editor/Field.tsx +++ b/apps/fronted/src/components/editor/Field.tsx @@ -1,19 +1,23 @@ "use client"; +import { useState } from "react"; import { CalendarIcon } from "lucide-react"; import { format } from "date-fns"; import { cn } from "@/lib/utils"; import { motion } from "framer-motion"; +import { Button } from "@/components/ui/button"; +import { Calendar } from "@/components/ui/calendar"; import { Popover, PopoverContent, PopoverTrigger, } from "@/components/ui/popover"; -import { Calendar } from "@/components/ui/calendar"; -import { Input } from "@/components/ui/input"; -import RichTextEditor from "@/components/shared/rich-editor/RichEditor"; -import { Button } from "../ui/button"; +import RichTextEditor from "../shared/rich-editor/RichEditor"; +import AIPolishDialog from "../shared/ai/AIPolishDialog"; +import { useAIConfigStore } from "@/store/useAIConfigStore"; +import { useRouter } from "next/navigation"; +import { toast } from "sonner"; -type FieldProps = { +interface FieldProps { label?: string; value?: string; onChange: (value: string) => void; @@ -21,9 +25,9 @@ type FieldProps = { placeholder?: string; required?: boolean; className?: string; -}; +} -const Field: React.FC = ({ +const Field = ({ label, value, onChange, @@ -31,72 +35,79 @@ const Field: React.FC = ({ placeholder, required, className, -}) => { - const renderLabel = () => ( - - {label} - {required && *} - - ); +}: FieldProps) => { + const router = useRouter(); + const [showPolishDialog, setShowPolishDialog] = useState(false); + const { doubaoModelId, doubaoApiKey } = useAIConfigStore(); + + const renderLabel = () => { + if (!label) return null; + return ( +
+ + {label} + {required && *} + +
+ ); + }; const inputStyles = cn( - "mt-1.5 block w-full transition-all duration-200", - "rounded-lg px-4 py-2.5", - "border" + "block w-full rounded-md border-0 py-1.5 px-3", + "text-gray-900 dark:text-neutral-300", + "shadow-sm ring-1 ring-inset ring-gray-300", + "placeholder:text-gray-400", + "focus:ring-2 focus:ring-inset focus:ring-primary", + "dark:bg-neutral-900/30 dark:ring-neutral-700 dark:focus:ring-primary", + "sm:text-sm sm:leading-6", + className ); if (type === "date") { - const date = value ? new Date(value) : undefined; - return ( -