diff --git a/packages/backend/server/src/plugins/copilot/resolver.ts b/packages/backend/server/src/plugins/copilot/resolver.ts index f2e46f027b..d81a496de6 100644 --- a/packages/backend/server/src/plugins/copilot/resolver.ts +++ b/packages/backend/server/src/plugins/copilot/resolver.ts @@ -4,6 +4,7 @@ import { BadRequestException, NotFoundException } from '@nestjs/common'; import { Args, Field, + Float, ID, InputType, Mutation, @@ -205,16 +206,16 @@ class CopilotPromptConfigType { @Field(() => Boolean, { nullable: true }) jsonMode!: boolean | null; - @Field(() => Number, { nullable: true }) + @Field(() => Float, { nullable: true }) frequencyPenalty!: number | null; - @Field(() => Number, { nullable: true }) + @Field(() => Float, { nullable: true }) presencePenalty!: number | null; - @Field(() => Number, { nullable: true }) + @Field(() => Float, { nullable: true }) temperature!: number | null; - @Field(() => Number, { nullable: true }) + @Field(() => Float, { nullable: true }) topP!: number | null; } @@ -238,8 +239,8 @@ class CopilotPromptType { @Field(() => String) name!: string; - @Field(() => AvailableModels) - model!: AvailableModels; + @Field(() => String) + model!: string; @Field(() => String, { nullable: true }) action!: string | null; diff --git a/packages/backend/server/src/schema.gql b/packages/backend/server/src/schema.gql index 7288f8e09f..58d6a2f01d 100644 --- a/packages/backend/server/src/schema.gql +++ b/packages/backend/server/src/schema.gql @@ -61,19 +61,19 @@ enum CopilotModels { } input CopilotPromptConfigInput { - frequencyPenalty: Int + frequencyPenalty: Float jsonMode: Boolean - presencePenalty: Int - temperature: Int - topP: Int + presencePenalty: Float + temperature: Float + topP: Float } type CopilotPromptConfigType { - frequencyPenalty: Int + frequencyPenalty: Float jsonMode: Boolean - presencePenalty: Int - temperature: Int - topP: Int + presencePenalty: Float + temperature: Float + topP: Float } input CopilotPromptMessageInput { @@ -102,7 +102,7 @@ type CopilotPromptType { action: String config: CopilotPromptConfigType messages: [CopilotPromptMessageType!]! - model: CopilotModels! + model: String! name: String! } diff --git a/packages/frontend/admin/src/app.tsx b/packages/frontend/admin/src/app.tsx index be20ba24f2..f8e6be011b 100644 --- a/packages/frontend/admin/src/app.tsx +++ b/packages/frontend/admin/src/app.tsx @@ -51,6 +51,10 @@ export const router = _createBrowserRouter( path: '/admin/auth', lazy: () => import('./modules/auth'), }, + { + path: '/admin/ai', + lazy: () => import('./modules/ai'), + }, { path: '/admin/setup', lazy: () => import('./modules/setup'), diff --git a/packages/frontend/admin/src/modules/ai/discard-changes.tsx b/packages/frontend/admin/src/modules/ai/discard-changes.tsx new file mode 100644 index 0000000000..ba7696e771 --- /dev/null +++ b/packages/frontend/admin/src/modules/ai/discard-changes.tsx @@ -0,0 +1,44 @@ +import { Button } from '@affine/admin/components/ui/button'; +import { + Dialog, + DialogContent, + DialogDescription, + DialogFooter, + DialogHeader, + DialogTitle, +} from '@affine/admin/components/ui/dialog'; + +export const DiscardChanges = ({ + open, + onClose, + onConfirm, + onOpenChange, +}: { + open: boolean; + onClose: () => void; + onConfirm: () => void; + onOpenChange: (open: boolean) => void; +}) => { + return ( + + + + Discard Changes + + Changes to this prompt will not be saved. + + + + + + Cancel + + + Discard + + + + + + ); +}; diff --git a/packages/frontend/admin/src/modules/ai/edit-prompt.tsx b/packages/frontend/admin/src/modules/ai/edit-prompt.tsx new file mode 100644 index 0000000000..21350f5627 --- /dev/null +++ b/packages/frontend/admin/src/modules/ai/edit-prompt.tsx @@ -0,0 +1,146 @@ +import { Button } from '@affine/admin/components/ui/button'; +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 { CheckIcon, XIcon } from 'lucide-react'; +import { useCallback, useEffect, useMemo, useState } from 'react'; + +import { useRightPanel } from '../layout'; +import type { Prompt } from './prompts'; +import { usePrompt } from './use-prompt'; + +export function EditPrompt({ item }: { item: Prompt }) { + const { closePanel } = useRightPanel(); + + const [messages, setMessages] = useState(item.messages); + const { updatePrompt } = usePrompt(); + + const handleChange = useCallback( + (e: React.ChangeEvent, index: number) => { + const newMessages = [...messages]; + newMessages[index] = { + ...newMessages[index], + content: e.target.value, + }; + setMessages(newMessages); + }, + [messages] + ); + const handleClose = useCallback(() => { + setMessages(item.messages); + closePanel(); + }, [closePanel, item.messages]); + + const onConfirm = useCallback(() => { + updatePrompt({ name: item.name, messages }); + handleClose(); + }, [handleClose, item.name, messages, updatePrompt]); + + const disableSave = useMemo( + () => JSON.stringify(messages) === JSON.stringify(item.messages), + [item.messages, messages] + ); + + useEffect(() => { + setMessages(item.messages); + }, [item.messages]); + + return ( + + + + + + Edit Prompt + + + + + + + + + Name + {item.name} + + {item.action ? ( + + Action + + {item.action} + + + ) : null} + + Model + + {item.model} + + + {item.config ? ( + + Config + {Object.entries(item.config).map(([key, value], index) => ( + + {index !== 0 && } + {key} + + {value?.toString()} + + + ))} + + ) : null} + + + Messages + {messages.map((message, index) => ( + + {index !== 0 && } + + Role + + {message.role} + + + + {message.params ? ( + + Params + {Object.entries(message.params).map(([key, value], index) => ( + + {index !== 0 && } + {key} + + {value.toString()} + + + ))} + + ) : null} + Content + handleChange(e, index)} + /> + + ))} + + + + ); +} diff --git a/packages/frontend/admin/src/modules/ai/index.tsx b/packages/frontend/admin/src/modules/ai/index.tsx new file mode 100644 index 0000000000..002fab636e --- /dev/null +++ b/packages/frontend/admin/src/modules/ai/index.tsx @@ -0,0 +1,41 @@ +import { Separator } from '@affine/admin/components/ui/separator'; +import { cn } from '@affine/admin/utils'; +import * as ScrollAreaPrimitive from '@radix-ui/react-scroll-area'; + +import { Prompts } from './prompts'; + +export function Ai() { + return null; + + // hide ai config in admin until it's ready + // return } />; +} + +export function AiPage() { + return ( + + + AI + + + + + + + + + + + + + ); +} +export { Ai as Component }; diff --git a/packages/frontend/admin/src/modules/ai/keys.tsx b/packages/frontend/admin/src/modules/ai/keys.tsx new file mode 100644 index 0000000000..d101327b21 --- /dev/null +++ b/packages/frontend/admin/src/modules/ai/keys.tsx @@ -0,0 +1,69 @@ +import { Button } from '@affine/admin/components/ui/button'; +import { Input } from '@affine/admin/components/ui/input'; +import { Label } from '@affine/admin/components/ui/label'; +import { Separator } from '@affine/admin/components/ui/separator'; +import { useState } from 'react'; + +export function Keys() { + const [openAIKey, setOpenAIKey] = useState(''); + const [falAIKey, setFalAIKey] = useState(''); + const [unsplashKey, setUnsplashKey] = useState(''); + + return ( + + + Keys + + + + + OpenAI Key + + setOpenAIKey(e.target.value)} + /> + Save + + + + + Fal.AI Key + + setFalAIKey(e.target.value)} + /> + Save + + + + + Unsplash Key + + setUnsplashKey(e.target.value)} + /> + Save + + + + + Custom API keys may not perform as expected. AFFiNE does not + guarantee results when using custom API keys. + + + + + ); +} diff --git a/packages/frontend/admin/src/modules/ai/prompts.tsx b/packages/frontend/admin/src/modules/ai/prompts.tsx new file mode 100644 index 0000000000..e7d075833d --- /dev/null +++ b/packages/frontend/admin/src/modules/ai/prompts.tsx @@ -0,0 +1,113 @@ +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 { useRightPanel } from '../layout'; +import { DiscardChanges } from './discard-changes'; +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'; + jsonMode: boolean | null; + frequencyPenalty: number | null; + presencePenalty: number | null; + temperature: number | null; + topP: number | null; + } | null; + messages: Array<{ + __typename?: 'CopilotPromptMessageType'; + role: CopilotPromptMessageRole; + content: string; + params: Record | null; + }>; +}; + +export function Prompts() { + const { prompts: list } = usePrompt(); + return ( + + + Prompts + + + + {list.map((item, index) => ( + + ))} + + + + ); +} + +export const PromptRow = ({ item, index }: { item: Prompt; index: number }) => { + const { setRightPanelContent, openPanel, isOpen } = useRightPanel(); + const [dialogOpen, setDialogOpen] = useState(false); + + const handleDiscardChangesCancel = useCallback(() => { + setDialogOpen(false); + }, []); + + const handleConfirm = useCallback( + (item: Prompt) => { + setRightPanelContent(); + if (dialogOpen) { + handleDiscardChangesCancel(); + } + + if (!isOpen) { + openPanel(); + } + }, + [ + dialogOpen, + handleDiscardChangesCancel, + isOpen, + openPanel, + setRightPanelContent, + ] + ); + + const handleEdit = useCallback( + (item: Prompt) => { + if (isOpen) { + setDialogOpen(true); + } else { + handleConfirm(item); + } + }, + [handleConfirm, isOpen] + ); + return ( + + {index !== 0 && } + handleEdit(item)} + > + {item.name} + + {item.messages.flatMap(message => message.content).join(' ')} + + + handleConfirm(item)} + /> + + ); +}; diff --git a/packages/frontend/admin/src/modules/ai/use-prompt.ts b/packages/frontend/admin/src/modules/ai/use-prompt.ts new file mode 100644 index 0000000000..bf38ea4a7b --- /dev/null +++ b/packages/frontend/admin/src/modules/ai/use-prompt.ts @@ -0,0 +1,51 @@ +import { useAsyncCallback } from '@affine/core/hooks/affine-async-hooks'; +import { + useMutateQueryResource, + useMutation, +} from '@affine/core/hooks/use-mutation'; +import { useQuery } from '@affine/core/hooks/use-query'; +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, + }; +}; diff --git a/packages/frontend/admin/src/modules/layout.tsx b/packages/frontend/admin/src/modules/layout.tsx index f1125d07e5..53d03a7f34 100644 --- a/packages/frontend/admin/src/modules/layout.tsx +++ b/packages/frontend/admin/src/modules/layout.tsx @@ -81,7 +81,7 @@ export function Layout({ content }: LayoutProps) { const [open, setOpen] = useState(false); const rightPanelRef = useRef(null); - const [activeTab, setActiveTab] = useState('Accounts'); + const [activeTab, setActiveTab] = useState(''); const [activeSubTab, setActiveSubTab] = useState('auth'); const [currentModule, setCurrentModule] = useState('auth'); diff --git a/packages/frontend/admin/src/modules/nav/nav.tsx b/packages/frontend/admin/src/modules/nav/nav.tsx index 3efcaaa49b..cfbc932252 100644 --- a/packages/frontend/admin/src/modules/nav/nav.tsx +++ b/packages/frontend/admin/src/modules/nav/nav.tsx @@ -7,12 +7,7 @@ import { import { buttonVariants } from '@affine/admin/components/ui/button'; import { cn } from '@affine/admin/utils'; import * as ScrollAreaPrimitive from '@radix-ui/react-scroll-area'; -import { - ClipboardListIcon, - CpuIcon, - SettingsIcon, - UsersIcon, -} from 'lucide-react'; +import { ClipboardListIcon, SettingsIcon, UsersIcon } from 'lucide-react'; import { useEffect } from 'react'; import { Link } from 'react-router-dom'; @@ -28,8 +23,6 @@ const TabsMap: { [key: string]: string } = { settings: 'Settings', }; -const defaultTab = 'Accounts'; - export function Nav() { const { moduleList } = useGetServerRuntimeConfig(); const { activeTab, setActiveTab, setCurrentModule } = useNav(); @@ -42,7 +35,6 @@ export function Nav() { return; } } - setActiveTab(defaultTab); }, [setActiveTab]); return ( @@ -64,7 +56,8 @@ export function Nav() { Accounts - AI - + */} ; + frequencyPenalty: InputMaybe; jsonMode: InputMaybe; - presencePenalty: InputMaybe; - temperature: InputMaybe; - topP: InputMaybe; + presencePenalty: InputMaybe; + temperature: InputMaybe; + topP: InputMaybe; } export interface CopilotPromptConfigType { __typename?: 'CopilotPromptConfigType'; - frequencyPenalty: Maybe; + frequencyPenalty: Maybe; jsonMode: Maybe; - presencePenalty: Maybe; - temperature: Maybe; - topP: Maybe; + presencePenalty: Maybe; + temperature: Maybe; + topP: Maybe; } export interface CopilotPromptMessageInput { @@ -149,7 +149,7 @@ export interface CopilotPromptType { action: Maybe; config: Maybe; messages: Array; - model: CopilotModels; + model: Scalars['String']['output']; name: Scalars['String']['output']; } @@ -1684,6 +1684,32 @@ export type OauthProvidersQuery = { }; }; +export type GetPromptsQueryVariables = Exact<{ [key: string]: never }>; + +export type GetPromptsQuery = { + __typename?: 'Query'; + listCopilotPrompts: Array<{ + __typename?: 'CopilotPromptType'; + name: string; + model: string; + action: string | null; + config: { + __typename?: 'CopilotPromptConfigType'; + jsonMode: boolean | null; + frequencyPenalty: number | null; + presencePenalty: number | null; + temperature: number | null; + topP: number | null; + } | null; + messages: Array<{ + __typename?: 'CopilotPromptMessageType'; + role: CopilotPromptMessageRole; + content: string; + params: Record | null; + }>; + }>; +}; + export type GetServerRuntimeConfigQueryVariables = Exact<{ [key: string]: never; }>; @@ -2186,6 +2212,35 @@ export type UpdateAccountMutation = { }; }; +export type UpdatePromptMutationVariables = Exact<{ + name: Scalars['String']['input']; + messages: Array | CopilotPromptMessageInput; +}>; + +export type UpdatePromptMutation = { + __typename?: 'Mutation'; + updateCopilotPrompt: { + __typename?: 'CopilotPromptType'; + name: string; + model: string; + action: string | null; + config: { + __typename?: 'CopilotPromptConfigType'; + jsonMode: boolean | null; + frequencyPenalty: number | null; + presencePenalty: number | null; + temperature: number | null; + topP: number | null; + } | null; + messages: Array<{ + __typename?: 'CopilotPromptMessageType'; + role: CopilotPromptMessageRole; + content: string; + params: Record | null; + }>; + }; +}; + export type UpdateServerRuntimeConfigsMutationVariables = Exact<{ updates: Scalars['JSONObject']['input']; }>; @@ -2432,6 +2487,11 @@ export type Queries = variables: OauthProvidersQueryVariables; response: OauthProvidersQuery; } + | { + name: 'getPromptsQuery'; + variables: GetPromptsQueryVariables; + response: GetPromptsQuery; + } | { name: 'getServerRuntimeConfigQuery'; variables: GetServerRuntimeConfigQueryVariables; @@ -2729,6 +2789,11 @@ export type Mutations = variables: UpdateAccountMutationVariables; response: UpdateAccountMutation; } + | { + name: 'updatePromptMutation'; + variables: UpdatePromptMutationVariables; + response: UpdatePromptMutation; + } | { name: 'updateServerRuntimeConfigsMutation'; variables: UpdateServerRuntimeConfigsMutationVariables;