feat: refactor copilot module (#14537)

This commit is contained in:
DarkSky
2026-03-02 13:57:55 +08:00
committed by GitHub
parent 60acd81d4b
commit c5d622531c
92 changed files with 5759 additions and 2170 deletions
@@ -1,146 +0,0 @@
import { ScrollArea } from '@affine/admin/components/ui/scroll-area';
import { Separator } from '@affine/admin/components/ui/separator';
import { Textarea } from '@affine/admin/components/ui/textarea';
import { useCallback, useEffect, useMemo, useState } from 'react';
import { RightPanelHeader } from '../header';
import { useRightPanel } from '../panel/context';
import type { Prompt } from './prompts';
import { usePrompt } from './use-prompt';
export function EditPrompt({
item,
setCanSave,
}: {
item: Prompt;
setCanSave: (changed: boolean) => void;
}) {
const { closePanel } = useRightPanel();
const [messages, setMessages] = useState(item.messages);
const { updatePrompt } = usePrompt();
const disableSave = useMemo(
() => JSON.stringify(messages) === JSON.stringify(item.messages),
[item.messages, messages]
);
const handleChange = useCallback(
(e: React.ChangeEvent<HTMLTextAreaElement>, index: number) => {
const newMessages = [...messages];
newMessages[index] = {
...newMessages[index],
content: e.target.value,
};
setMessages(newMessages);
setCanSave(!disableSave);
},
[disableSave, messages, setCanSave]
);
const handleClose = useCallback(() => {
setMessages(item.messages);
closePanel();
}, [closePanel, item.messages]);
const onConfirm = useCallback(() => {
if (!disableSave) {
updatePrompt({ name: item.name, messages });
}
handleClose();
}, [disableSave, handleClose, item.name, messages, updatePrompt]);
useEffect(() => {
setMessages(item.messages);
}, [item.messages]);
return (
<div className="flex flex-col h-full gap-1">
<RightPanelHeader
title="Edit Prompt"
handleClose={handleClose}
handleConfirm={onConfirm}
canSave={!disableSave}
/>
<ScrollArea>
<div className="grid">
<div className="px-5 py-4 overflow-y-auto space-y-[10px] flex flex-col gap-5">
<div className="flex flex-col">
<div className="text-sm font-medium">Name</div>
<div className="text-sm font-normal text-muted-foreground">
{item.name}
</div>
</div>
{item.action ? (
<div className="flex flex-col">
<div className="text-sm font-medium">Action</div>
<div className="text-sm font-normal text-muted-foreground">
{item.action}
</div>
</div>
) : null}
<div className="flex flex-col">
<div className="text-sm font-medium">Model</div>
<div className="text-sm font-normal text-muted-foreground">
{item.model}
</div>
</div>
{item.config ? (
<div className="flex flex-col border rounded p-3">
<div className="text-sm font-medium">Config</div>
{Object.entries(item.config).map(([key, value], index) => (
<div key={key} className="flex flex-col">
{index !== 0 && <Separator />}
<span className="text-sm font-normal">{key}</span>
<span className="text-sm font-normal text-muted-foreground">
{value?.toString()}
</span>
</div>
))}
</div>
) : null}
</div>
<div className="px-5 py-4 overflow-y-auto space-y-[10px] flex flex-col">
<div className="text-sm font-medium">Messages</div>
{messages.map((message, index) => (
<div key={message.content} className="flex flex-col gap-3">
{index !== 0 && <Separator />}
<div>
<div className="text-sm font-normal">Role</div>
<div className="text-sm font-normal text-muted-foreground">
{message.role}
</div>
</div>
{message.params ? (
<div>
<div className="text-sm font-medium">Params</div>
{Object.entries(message.params).map(
([key, value], index) => (
<div key={key} className="flex flex-col">
{index !== 0 && <Separator />}
<span className="text-sm font-normal">{key}</span>
<span
className="text-sm font-normal text-muted-foreground"
style={{ overflowWrap: 'break-word' }}
>
{value.toString()}
</span>
</div>
)
)}
</div>
) : null}
<div className="text-sm font-normal">Content</div>
<Textarea
className=" min-h-48"
value={message.content}
onChange={e => handleChange(e, index)}
/>
</div>
))}
</div>
</div>
</ScrollArea>
</div>
);
}
@@ -32,7 +32,6 @@ function AiPage() {
/>
</div>
</div>
{/* <Prompts /> */}
</ScrollAreaPrimitive.Viewport>
<ScrollAreaPrimitive.ScrollAreaScrollbar
className={cn(
@@ -1,108 +0,0 @@
import { Button } from '@affine/admin/components/ui/button';
import { Separator } from '@affine/admin/components/ui/separator';
import type { CopilotPromptMessageRole } from '@affine/graphql';
import { useCallback, useState } from 'react';
import { DiscardChanges } from '../../components/shared/discard-changes';
import { useRightPanel } from '../panel/context';
import { EditPrompt } from './edit-prompt';
import { usePrompt } from './use-prompt';
export type Prompt = {
__typename?: 'CopilotPromptType';
name: string;
model: string;
action: string | null;
config: {
__typename?: 'CopilotPromptConfigType';
frequencyPenalty: number | null;
presencePenalty: number | null;
temperature: number | null;
topP: number | null;
} | null;
messages: Array<{
__typename?: 'CopilotPromptMessageType';
role: CopilotPromptMessageRole;
content: string;
params: Record<string, string> | null;
}>;
};
export function Prompts() {
const { prompts: list } = usePrompt();
return (
<div className="flex flex-col h-full gap-3 py-5 px-6 w-full">
<div className="flex items-center">
<span className="text-xl font-semibold">Prompts</span>
</div>
<div className="flex-grow overflow-y-auto space-y-[10px]">
<div className="flex flex-col rounded-md border w-full">
{list.map((item, index) => (
<PromptRow
key={`${item.name}-${index}`}
item={item}
index={index}
/>
))}
</div>
</div>
</div>
);
}
export const PromptRow = ({ item, index }: { item: Prompt; index: number }) => {
const { setPanelContent, openPanel, isOpen } = useRightPanel();
const [dialogOpen, setDialogOpen] = useState(false);
const [canSave, setCanSave] = useState(false);
const handleDiscardChangesCancel = useCallback(() => {
setDialogOpen(false);
setCanSave(false);
}, []);
const handleConfirm = useCallback(
(item: Prompt) => {
setPanelContent(<EditPrompt item={item} setCanSave={setCanSave} />);
if (dialogOpen) {
handleDiscardChangesCancel();
}
if (!isOpen) {
openPanel();
}
},
[dialogOpen, handleDiscardChangesCancel, isOpen, openPanel, setPanelContent]
);
const handleEdit = useCallback(
(item: Prompt) => {
if (isOpen && canSave) {
setDialogOpen(true);
} else {
handleConfirm(item);
}
},
[canSave, handleConfirm, isOpen]
);
return (
<div>
{index !== 0 && <Separator />}
<Button
variant="ghost"
className="flex flex-col gap-1 w-full items-start px-6 py-[14px] h-full "
onClick={() => handleEdit(item)}
>
<div>{item.name}</div>
<div className="text-left w-full opacity-50 overflow-hidden text-ellipsis whitespace-nowrap break-words text-nowrap">
{item.messages.flatMap(message => message.content).join(' ')}
</div>
</Button>
<DiscardChanges
open={dialogOpen}
onOpenChange={setDialogOpen}
onClose={handleDiscardChangesCancel}
onConfirm={() => handleConfirm(item)}
/>
</div>
);
};
@@ -1,51 +0,0 @@
import {
useMutateQueryResource,
useMutation,
} from '@affine/admin/use-mutation';
import { useQuery } from '@affine/admin/use-query';
import { useAsyncCallback } from '@affine/core/components/hooks/affine-async-hooks';
import { getPromptsQuery, updatePromptMutation } from '@affine/graphql';
import { toast } from 'sonner';
import type { Prompt } from './prompts';
export const usePrompt = () => {
const { data } = useQuery({
query: getPromptsQuery,
});
const { trigger } = useMutation({
mutation: updatePromptMutation,
});
const revalidate = useMutateQueryResource();
const updatePrompt = useAsyncCallback(
async ({
name,
messages,
}: {
name: string;
messages: Prompt['messages'];
}) => {
await trigger({
name,
messages,
})
.then(async () => {
await revalidate(getPromptsQuery);
toast.success('Prompt updated successfully');
})
.catch(e => {
toast(e.message);
console.error(e);
});
},
[revalidate, trigger]
);
return {
prompts: data.listCopilotPrompts,
updatePrompt,
};
};