ai改写调用

This commit is contained in:
董宏凯 committed 2026-05-17 20:18:11 +08:00
1 parent a0a6227368
commit 05ff10df1b
4 files changed
+106 -15

No files matched your search

+3 -5
View File
@@ -1,7 +1,5 @@
import { apiFetch } from "@/core/api/fetch-client";
import { env } from "@/env";
const BASE = (env.NEXT_PUBLIC_BACKEND_BASE_URL as string | undefined) ?? "http://localhost:8001";
import { getBackendBaseURL } from "@/core/config";
export type RewriteAction = "polish" | "expand" | "shorten" | "tone";
export type RewriteTone =
@@ -12,7 +10,7 @@ export type RewriteTone =
| "expressive";
export interface RewriteRequest {
threadId: string;
threadId?: string | null;
selection: { from: number; to: number; text: string };
action: RewriteAction;
tone?: RewriteTone;
@@ -71,7 +69,7 @@ export async function* streamRewrite(
req: RewriteRequest,
signal?: AbortSignal,
): AsyncGenerator<RewriteStreamEvent> {
const res = await apiFetch(`${BASE}/api/writing/rewrite`, {
const res = await apiFetch(`${getBackendBaseURL()}/api/writing/rewrite`, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(req),
@@ -1,6 +1,6 @@
import { useCallback, useState } from 'react';
import { useCallback, useEffect, useRef, useState } from 'react';
import { useNavigate } from 'react-router-dom';
import { Plus, RotateCcw, Settings2, Square } from 'lucide-react';
import { ChevronsDown, Plus, RotateCcw, Settings2, Square } from 'lucide-react';
import { Button } from '@/components/ui/button';
import { cn } from '@/lib/utils';
@@ -50,6 +50,84 @@ export function AIWritingPanel() {
const isRunning = !isIdle && !isDone && !isError;
const showSkeleton = isRunning && !state.currentPause;
// ── 自动滚底逻辑 ────────────────────────────────────────────────────────────
const scrollRef = useRef<HTMLDivElement>(null);
// 程序触发滚动时置 true,阻止 scroll 事件误判为用户上划
const isAutoScrollingRef = useRef(false);
// 同步 ref:让 scroll/wheel 回调可以立即读取最新值,不依赖 state 异步更新
const autoScrollRef = useRef(true);
const [autoScroll, setAutoScroll] = useState(true);
const disableAutoScroll = useCallback(() => {
autoScrollRef.current = false;
setAutoScroll(false);
}, []);
// 新会话 / 重置后恢复自动滚底
useEffect(() => {
if (isIdle) {
autoScrollRef.current = true;
setAutoScroll(true);
}
}, [isIdle]);
// 监听用户手动滚动(挂载一次)
useEffect(() => {
const el = scrollRef.current;
if (!el) return;
// wheel 事件:鼠标滚轮 / 触控板,只由用户触发
const onWheel = (e: WheelEvent) => {
if (e.deltaY < 0) disableAutoScroll(); // 向上滚
};
// scroll 事件:兼容拖动滚动条的场景
// 通过对比前后 scrollTop 判断是否向上滚(排除程序触发的向下滚)
let lastTop = el.scrollTop;
const onScroll = () => {
const cur = el.scrollTop;
if (!isAutoScrollingRef.current && cur < lastTop - 2) {
disableAutoScroll();
}
lastTop = cur;
};
el.addEventListener('wheel', onWheel, { passive: true });
el.addEventListener('scroll', onScroll, { passive: true });
return () => {
el.removeEventListener('wheel', onWheel);
el.removeEventListener('scroll', onScroll);
};
}, [disableAutoScroll]);
// 内容变化时若自动滚底开启则滚到底部
useEffect(() => {
// 检查同步 ref,避免 state 异步导致的时序竞争
if (!autoScrollRef.current || !isRunning || !scrollRef.current) return;
const el = scrollRef.current;
isAutoScrollingRef.current = true;
el.scrollTop = el.scrollHeight;
requestAnimationFrame(() => { isAutoScrollingRef.current = false; });
}, [
autoScroll, // 点击按钮后重新触发一次
isRunning,
state.progressEvents.length,
state.streamingDraftSections.length,
state.streamingReviewItems.length,
state.outlineStreamingText,
state.currentPause,
]);
const scrollToBottom = useCallback(() => {
const el = scrollRef.current;
if (!el) return;
isAutoScrollingRef.current = true;
el.scrollTop = el.scrollHeight;
requestAnimationFrame(() => { isAutoScrollingRef.current = false; });
autoScrollRef.current = true;
setAutoScroll(true);
}, []);
const handleSelectHistory = useCallback((session: AIWritingSessionSummary) => {
setHistorySession(session);
// Push historical draft to the right panel
@@ -158,8 +236,9 @@ export function AIWritingPanel() {
</div>
</div>
{/* 主体:可滚动 */}
<div className="flex-1 overflow-y-auto">
{/* 主体:relative 父容器 + absolute 滚动容器,确保滚动容器尺寸始终正确 */}
<div className="flex-1 min-h-0 relative">
<div ref={scrollRef} className="absolute inset-0 overflow-y-auto">
{/* 未开始时显示表单 */}
{isIdle && (
<AIWritingForm onSubmit={startWriting} />
@@ -233,7 +312,21 @@ export function AIWritingPanel() {
{state.error}
</div>
)}
</div>
</div>{/* 滚动内容区结束 */}
{/* 回到底部悬浮按钮:生成中且用户已上划时出现 */}
{isRunning && !autoScroll && (
<div className="absolute inset-x-0 bottom-4 flex justify-center z-10 pointer-events-none">
<button
onClick={scrollToBottom}
className="pointer-events-auto flex items-center gap-1.5 px-3 py-1.5 text-xs rounded-full bg-background border shadow-md hover:bg-muted transition-colors whitespace-nowrap"
>
<ChevronsDown className="size-3.5" />
回到底部
</button>
</div>
)}
</div>{/* 相对定位容器结束 */}
{/* 状态栏 */}
{!isIdle && (
@@ -122,7 +122,7 @@ export function useWritingRewrite(editor: Editor | null) {
const rewrite = useCallback(
async (action: RewriteAction, tone?: RewriteTone, customInstruction?: string) => {
if (!writingSelection || !threadId || !editor) return;
if (!writingSelection || !editor) return;
abortRef.current?.abort();
const abort = new AbortController();
@@ -152,12 +152,12 @@ export function useWritingRewrite(editor: Editor | null) {
await runStream(req, abort);
},
// eslint-disable-next-line react-hooks/exhaustive-deps
[writingSelection, threadId, modelName, editor, runStream],
[writingSelection, modelName, editor, runStream],
);
const rewriteFollowUp = useCallback(
async (instruction: string, action: RewriteAction, tone?: RewriteTone) => {
if (!writingSelection || !threadId || !editor) return;
if (!writingSelection || !editor) return;
const prevResult = result;
if (!prevResult) return;
@@ -190,7 +190,7 @@ export function useWritingRewrite(editor: Editor | null) {
await runStream(req, abort);
},
// eslint-disable-next-line react-hooks/exhaustive-deps
[writingSelection, threadId, modelName, editor, result, runStream],
[writingSelection, modelName, editor, result, runStream],
);
const reset = useCallback(() => {
@@ -70,7 +70,7 @@ class SurroundingContext(BaseModel):
class RewriteRequest(BaseModel):
thread_id: str = Field(..., alias="threadId")
thread_id: str | None = Field(default=None, alias="threadId")
selection: SelectionInfo
action: str = Field(..., description="polish | expand | shorten | tone")
tone: str | None = Field(default=None)