mirror of
https://github.com/docmost/docmost.git
synced 2026-05-07 14:43:06 +08:00
Compare commits
2 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| bb83d12c8b | |||
| 0f29eb8842 |
@@ -0,0 +1,61 @@
|
|||||||
|
import { useState, useCallback, useRef } from "react";
|
||||||
|
import { useAiGenerateStreamMutation } from "@/ee/ai/queries/ai-query.ts";
|
||||||
|
import { AiGenerateDto } from "@/ee/ai/types/ai.types.ts";
|
||||||
|
|
||||||
|
export function useAiStream() {
|
||||||
|
const [content, setContent] = useState("");
|
||||||
|
const [isStreaming, setIsStreaming] = useState(false);
|
||||||
|
const abortControllerRef = useRef<AbortController | null>(null);
|
||||||
|
const mutation = useAiGenerateStreamMutation();
|
||||||
|
|
||||||
|
const startStream = useCallback(
|
||||||
|
async (data: AiGenerateDto) => {
|
||||||
|
setContent("");
|
||||||
|
setIsStreaming(true);
|
||||||
|
|
||||||
|
try {
|
||||||
|
const controller = await mutation.mutateAsync({
|
||||||
|
...data,
|
||||||
|
onChunk: (chunk) => {
|
||||||
|
setContent((prev) => prev + chunk.content);
|
||||||
|
},
|
||||||
|
onError: (error) => {
|
||||||
|
console.error("AI stream error:", error);
|
||||||
|
setIsStreaming(false);
|
||||||
|
},
|
||||||
|
onComplete: () => {
|
||||||
|
setIsStreaming(false);
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
abortControllerRef.current = controller;
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Failed to start stream:", error);
|
||||||
|
setIsStreaming(false);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
[mutation]
|
||||||
|
);
|
||||||
|
|
||||||
|
const stopStream = useCallback(() => {
|
||||||
|
if (abortControllerRef.current) {
|
||||||
|
abortControllerRef.current.abort();
|
||||||
|
abortControllerRef.current = null;
|
||||||
|
setIsStreaming(false);
|
||||||
|
}
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const resetContent = useCallback(() => {
|
||||||
|
setContent("");
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
return {
|
||||||
|
content,
|
||||||
|
isStreaming,
|
||||||
|
startStream,
|
||||||
|
stopStream,
|
||||||
|
resetContent,
|
||||||
|
isLoading: mutation.isPending,
|
||||||
|
error: mutation.error,
|
||||||
|
};
|
||||||
|
}
|
||||||
@@ -0,0 +1,45 @@
|
|||||||
|
import {
|
||||||
|
useMutation,
|
||||||
|
UseMutationResult,
|
||||||
|
useQuery,
|
||||||
|
UseQueryResult,
|
||||||
|
} from "@tanstack/react-query";
|
||||||
|
import {
|
||||||
|
generateAiContent,
|
||||||
|
generateAiContentStream,
|
||||||
|
getAiConfig,
|
||||||
|
} from "@/ee/ai/services/ai-service.ts";
|
||||||
|
import {
|
||||||
|
AiConfigResponse,
|
||||||
|
AiContentResponse,
|
||||||
|
AiGenerateDto,
|
||||||
|
AiStreamChunk,
|
||||||
|
AiStreamError,
|
||||||
|
} from "@/ee/ai/types/ai.types.ts";
|
||||||
|
|
||||||
|
export function useAiGenerateMutation(): UseMutationResult<
|
||||||
|
AiContentResponse,
|
||||||
|
Error,
|
||||||
|
AiGenerateDto
|
||||||
|
> {
|
||||||
|
return useMutation({
|
||||||
|
mutationFn: (data: AiGenerateDto) => generateAiContent(data),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
interface StreamCallbacks {
|
||||||
|
onChunk: (chunk: AiStreamChunk) => void;
|
||||||
|
onError?: (error: AiStreamError) => void;
|
||||||
|
onComplete?: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function useAiGenerateStreamMutation(): UseMutationResult<
|
||||||
|
AbortController,
|
||||||
|
Error,
|
||||||
|
AiGenerateDto & StreamCallbacks
|
||||||
|
> {
|
||||||
|
return useMutation({
|
||||||
|
mutationFn: ({ onChunk, onError, onComplete, ...data }) =>
|
||||||
|
generateAiContentStream(data, onChunk, onError, onComplete),
|
||||||
|
});
|
||||||
|
}
|
||||||
@@ -0,0 +1,89 @@
|
|||||||
|
import api from "@/lib/api-client.ts";
|
||||||
|
import {
|
||||||
|
AiGenerateDto,
|
||||||
|
AiContentResponse,
|
||||||
|
AiStreamChunk,
|
||||||
|
AiStreamError,
|
||||||
|
} from "@/ee/ai/types/ai.types.ts";
|
||||||
|
|
||||||
|
export async function generateAiContent(
|
||||||
|
data: AiGenerateDto,
|
||||||
|
): Promise<AiContentResponse> {
|
||||||
|
const req = await api.post<AiContentResponse>("/ai/generate", data);
|
||||||
|
return req.data;
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function generateAiContentStream(
|
||||||
|
data: AiGenerateDto,
|
||||||
|
onChunk: (chunk: AiStreamChunk) => void,
|
||||||
|
onError?: (error: AiStreamError) => void,
|
||||||
|
onComplete?: () => void,
|
||||||
|
): Promise<AbortController> {
|
||||||
|
const abortController = new AbortController();
|
||||||
|
try {
|
||||||
|
const response = await fetch("/api/ai/generate/stream", {
|
||||||
|
method: "POST",
|
||||||
|
headers: {
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
},
|
||||||
|
body: JSON.stringify(data),
|
||||||
|
signal: abortController.signal,
|
||||||
|
credentials: "include", // This ensures cookies are sent, matching axios withCredentials
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
throw new Error(`HTTP error! status: ${response.status}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
const reader = response.body?.getReader();
|
||||||
|
const decoder = new TextDecoder();
|
||||||
|
|
||||||
|
if (!reader) {
|
||||||
|
throw new Error("Response body is not readable");
|
||||||
|
}
|
||||||
|
|
||||||
|
const processStream = async () => {
|
||||||
|
try {
|
||||||
|
while (true) {
|
||||||
|
const { done, value } = await reader.read();
|
||||||
|
if (done) break;
|
||||||
|
|
||||||
|
const chunk = decoder.decode(value, { stream: true });
|
||||||
|
const lines = chunk.split("\n");
|
||||||
|
|
||||||
|
for (const line of lines) {
|
||||||
|
if (line.startsWith("data: ")) {
|
||||||
|
const data = line.slice(6);
|
||||||
|
if (data === "[DONE]") {
|
||||||
|
onComplete?.();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
const parsed = JSON.parse(data);
|
||||||
|
if (parsed.error) {
|
||||||
|
onError?.(parsed);
|
||||||
|
} else {
|
||||||
|
onChunk(parsed);
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
// Ignore parse errors for incomplete chunks
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
if (error.name !== "AbortError") {
|
||||||
|
onError?.({ error: error.message });
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
reader.releaseLock();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
processStream();
|
||||||
|
} catch (error) {
|
||||||
|
onError?.({ error: error.message });
|
||||||
|
}
|
||||||
|
|
||||||
|
return abortController;
|
||||||
|
}
|
||||||
@@ -0,0 +1,40 @@
|
|||||||
|
export enum AiAction {
|
||||||
|
IMPROVE_WRITING = "improve_writing",
|
||||||
|
FIX_SPELLING_GRAMMAR = "fix_spelling_grammar",
|
||||||
|
MAKE_SHORTER = "make_shorter",
|
||||||
|
MAKE_LONGER = "make_longer",
|
||||||
|
SIMPLIFY = "simplify",
|
||||||
|
CHANGE_TONE = "change_tone",
|
||||||
|
SUMMARIZE = "summarize",
|
||||||
|
CONTINUE_WRITING = "continue_writing",
|
||||||
|
TRANSLATE = "translate",
|
||||||
|
CUSTOM = "custom",
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface AiGenerateDto {
|
||||||
|
action?: AiAction;
|
||||||
|
content: string;
|
||||||
|
prompt?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface AiContentResponse {
|
||||||
|
content: string;
|
||||||
|
usage?: {
|
||||||
|
promptTokens: number;
|
||||||
|
completionTokens: number;
|
||||||
|
totalTokens: number;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface AiConfigResponse {
|
||||||
|
configured: boolean;
|
||||||
|
availableActions: AiAction[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface AiStreamChunk {
|
||||||
|
content: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface AiStreamError {
|
||||||
|
error: string;
|
||||||
|
}
|
||||||
@@ -70,6 +70,7 @@
|
|||||||
"nanoid": "3.3.11",
|
"nanoid": "3.3.11",
|
||||||
"nestjs-kysely": "^1.2.0",
|
"nestjs-kysely": "^1.2.0",
|
||||||
"nodemailer": "^7.0.3",
|
"nodemailer": "^7.0.3",
|
||||||
|
"openai": "^5.12.2",
|
||||||
"openid-client": "^5.7.1",
|
"openid-client": "^5.7.1",
|
||||||
"otpauth": "^9.4.0",
|
"otpauth": "^9.4.0",
|
||||||
"p-limit": "^6.2.0",
|
"p-limit": "^6.2.0",
|
||||||
|
|||||||
+1
-1
Submodule apps/server/src/ee updated: fbc01d808f...4100345c18
@@ -213,4 +213,16 @@ export class EnvironmentService {
|
|||||||
getPostHogKey(): string {
|
getPostHogKey(): string {
|
||||||
return this.configService.get<string>('POSTHOG_KEY');
|
return this.configService.get<string>('POSTHOG_KEY');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
getOpenAiApiKey(): string {
|
||||||
|
return this.configService.get<string>('OPENAI_API_KEY');
|
||||||
|
}
|
||||||
|
|
||||||
|
getOpenAiApiUrl(): string {
|
||||||
|
return this.configService.get<string>('OPENAI_API_URL');
|
||||||
|
}
|
||||||
|
|
||||||
|
getOpenAiModel(): string {
|
||||||
|
return this.configService.get<string>('OPENAI_MODEL');
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Generated
+20
@@ -543,6 +543,9 @@ importers:
|
|||||||
nodemailer:
|
nodemailer:
|
||||||
specifier: ^7.0.3
|
specifier: ^7.0.3
|
||||||
version: 7.0.3
|
version: 7.0.3
|
||||||
|
openai:
|
||||||
|
specifier: ^5.12.2
|
||||||
|
version: 5.12.2(ws@8.18.2)(zod@3.25.56)
|
||||||
openid-client:
|
openid-client:
|
||||||
specifier: ^5.7.1
|
specifier: ^5.7.1
|
||||||
version: 5.7.1
|
version: 5.7.1
|
||||||
@@ -7655,6 +7658,18 @@ packages:
|
|||||||
resolution: {integrity: sha512-7x81NCL719oNbsq/3mh+hVrAWmFuEYUqrq/Iw3kUzH8ReypT9QQ0BLoJS7/G9k6N81XjW4qHWtjWwe/9eLy1EQ==}
|
resolution: {integrity: sha512-7x81NCL719oNbsq/3mh+hVrAWmFuEYUqrq/Iw3kUzH8ReypT9QQ0BLoJS7/G9k6N81XjW4qHWtjWwe/9eLy1EQ==}
|
||||||
engines: {node: '>=12'}
|
engines: {node: '>=12'}
|
||||||
|
|
||||||
|
openai@5.12.2:
|
||||||
|
resolution: {integrity: sha512-xqzHHQch5Tws5PcKR2xsZGX9xtch+JQFz5zb14dGqlshmmDAFBFEWmeIpf7wVqWV+w7Emj7jRgkNJakyKE0tYQ==}
|
||||||
|
hasBin: true
|
||||||
|
peerDependencies:
|
||||||
|
ws: ^8.18.0
|
||||||
|
zod: ^3.23.8
|
||||||
|
peerDependenciesMeta:
|
||||||
|
ws:
|
||||||
|
optional: true
|
||||||
|
zod:
|
||||||
|
optional: true
|
||||||
|
|
||||||
openid-client@5.7.1:
|
openid-client@5.7.1:
|
||||||
resolution: {integrity: sha512-jDBPgSVfTnkIh71Hg9pRvtJc6wTwqjRkN88+gCFtYWrlP4Yx2Dsrow8uPi3qLr/aeymPF3o2+dS+wOpglK04ew==}
|
resolution: {integrity: sha512-jDBPgSVfTnkIh71Hg9pRvtJc6wTwqjRkN88+gCFtYWrlP4Yx2Dsrow8uPi3qLr/aeymPF3o2+dS+wOpglK04ew==}
|
||||||
|
|
||||||
@@ -18262,6 +18277,11 @@ snapshots:
|
|||||||
is-docker: 2.2.1
|
is-docker: 2.2.1
|
||||||
is-wsl: 2.2.0
|
is-wsl: 2.2.0
|
||||||
|
|
||||||
|
openai@5.12.2(ws@8.18.2)(zod@3.25.56):
|
||||||
|
optionalDependencies:
|
||||||
|
ws: 8.18.2
|
||||||
|
zod: 3.25.56
|
||||||
|
|
||||||
openid-client@5.7.1:
|
openid-client@5.7.1:
|
||||||
dependencies:
|
dependencies:
|
||||||
jose: 4.15.9
|
jose: 4.15.9
|
||||||
|
|||||||
Reference in New Issue
Block a user