ai改写调用
This commit is contained in:
1 parent
a0a6227368
commit
05ff10df1b
4 files changed
+106
-15
No files matched your search
@@ -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)
|
||||
|
||||
Reference in new issue
Block a user