diff --git a/.husky/pre-commit b/.husky/pre-commit new file mode 100644 index 0000000..2d6a688 --- /dev/null +++ b/.husky/pre-commit @@ -0,0 +1,2 @@ + +npm run lint-staged diff --git a/apps/desktop/app/app.css b/apps/desktop/app/app.css index aa2f10f..bffe18d 100644 --- a/apps/desktop/app/app.css +++ b/apps/desktop/app/app.css @@ -1,2 +1,2 @@ -@import "tailwindcss"; -@import "@tsumugi/ui/index.css"; +@import 'tailwindcss'; +@import '@tsumugi/ui/index.css'; diff --git a/apps/desktop/app/constants/path.ts b/apps/desktop/app/constants/path.ts index c825888..1070721 100644 --- a/apps/desktop/app/constants/path.ts +++ b/apps/desktop/app/constants/path.ts @@ -1,3 +1,3 @@ export const PATH_TOP = '/'; export const PATH_HOME = '/home'; -export const PATH_WORKSPACE = '/workspace'; \ No newline at end of file +export const PATH_WORKSPACE = '/workspace'; diff --git a/apps/desktop/app/hooks/characters.ts b/apps/desktop/app/hooks/characters.ts index 9675a25..7219890 100644 --- a/apps/desktop/app/hooks/characters.ts +++ b/apps/desktop/app/hooks/characters.ts @@ -6,7 +6,6 @@ import type { ContentItemKey, ContentTreeKey } from '~/hooks/keys'; type CharacterTreeKey = ContentTreeKey<'character'>; - /** * プロジェクト内のキャラクターツリーを取得する * @param projectId - プロジェクトID @@ -26,7 +25,10 @@ type CharacterKey = ContentItemKey<'character'>; * @param id - キャラクターID * @param config */ -export function useCharacter(id: string, config?: SWRConfiguration) { +export function useCharacter( + id: string, + config?: SWRConfiguration, +) { const adapter = useAdapter(); return useSWR( { type: 'character', id }, @@ -122,11 +124,13 @@ interface ReorderCharactersArg { */ export function useReorderCharacters(projectId: string) { const adapter = useAdapter(); - return useSWRMutation( - { type: 'characterTree', projectId }, - async (_, { arg }) => { - await adapter.characters.reorder(arg.parentId, arg.orderedIds); - return undefined; - }, - ); + return useSWRMutation< + undefined, + Error, + CharacterTreeKey, + ReorderCharactersArg + >({ type: 'characterTree', projectId }, async (_, { arg }) => { + await adapter.characters.reorder(arg.parentId, arg.orderedIds); + return undefined; + }); } diff --git a/apps/desktop/app/hooks/memos.ts b/apps/desktop/app/hooks/memos.ts index bfa7c9a..63aa554 100644 --- a/apps/desktop/app/hooks/memos.ts +++ b/apps/desktop/app/hooks/memos.ts @@ -6,15 +6,15 @@ import type { ContentItemKey, ContentTreeKey } from '~/hooks/keys'; type MemoTreeKey = ContentTreeKey<'memo'>; - /** * プロジェクト内のメモツリーを取得する * @param projectId - プロジェクトID */ export function useMemoTree(projectId: string) { const adapter = useAdapter(); - return useSWR({ type: 'memoTree', projectId }, ({ projectId }) => - adapter.memos.getTreeByProjectId(projectId), + return useSWR( + { type: 'memoTree', projectId }, + ({ projectId }) => adapter.memos.getTreeByProjectId(projectId), ); } @@ -25,7 +25,10 @@ type MemoKey = ContentItemKey<'memo'>; * @param id - メモID * @param config */ -export function useMemo(id: string, config?: SWRConfiguration) { +export function useMemo( + id: string, + config?: SWRConfiguration, +) { const adapter = useAdapter(); return useSWR( { type: 'memo', id }, @@ -34,7 +37,10 @@ export function useMemo(id: string, config?: SWRConfiguration; +type CreateMemoData = Omit< + Memo, + 'projectId' | 'id' | 'createdAt' | 'updatedAt' +>; /** * メモを作成する @@ -45,14 +51,17 @@ export function useCreateMemo(projectId: string) { const adapter = useAdapter(); return useSWRMutation( { type: 'memoTree', projectId }, - ({ projectId }, { arg }) => adapter.memos.create({ - projectId, - ...arg, - }), + ({ projectId }, { arg }) => + adapter.memos.create({ + projectId, + ...arg, + }), ); } -type UpdateMemoData = Partial>; +type UpdateMemoData = Partial< + Omit +>; /** * メモを更新する @@ -134,7 +143,8 @@ interface MemosByTagKey { */ export function useMemosByTag(projectId: string, tag: string) { const adapter = useAdapter(); - return useSWR({ type: 'memosByTag', projectId, tag }, ({ projectId, tag }) => - adapter.memos.getByTag(projectId, tag), + return useSWR( + { type: 'memosByTag', projectId, tag }, + ({ projectId, tag }) => adapter.memos.getByTag(projectId, tag), ); } diff --git a/apps/desktop/app/hooks/plots.ts b/apps/desktop/app/hooks/plots.ts index dc48635..0d88953 100644 --- a/apps/desktop/app/hooks/plots.ts +++ b/apps/desktop/app/hooks/plots.ts @@ -6,15 +6,15 @@ import type { ContentItemKey, ContentTreeKey } from '~/hooks/keys'; type PlotTreeKey = ContentTreeKey<'plot'>; - /** * プロジェクト内のプロットツリーを取得する * @param projectId - プロジェクトID */ export function usePlotTree(projectId: string) { const adapter = useAdapter(); - return useSWR({ type: 'plotTree', projectId }, ({ projectId }) => - adapter.plots.getTreeByProjectId(projectId), + return useSWR( + { type: 'plotTree', projectId }, + ({ projectId }) => adapter.plots.getTreeByProjectId(projectId), ); } @@ -25,7 +25,10 @@ type PlotKey = ContentItemKey<'plot'>; * @param id - プロットID * @param config */ -export function usePlot(id: string, config?: SWRConfiguration) { +export function usePlot( + id: string, + config?: SWRConfiguration, +) { const adapter = useAdapter(); return useSWR( { type: 'plot', id }, @@ -34,7 +37,10 @@ export function usePlot(id: string, config?: SWRConfiguration; +type CreatePlotData = Omit< + Plot, + 'projectId' | 'id' | 'createdAt' | 'updatedAt' +>; /** * プロットを作成する @@ -45,14 +51,17 @@ export function useCreatePlot(projectId: string) { const adapter = useAdapter(); return useSWRMutation( { type: 'plotTree', projectId }, - ({ projectId }, { arg }) => adapter.plots.create({ - projectId, - ...arg, - }), + ({ projectId }, { arg }) => + adapter.plots.create({ + projectId, + ...arg, + }), ); } -type UpdatePlotData = Partial>; +type UpdatePlotData = Partial< + Omit +>; /** * プロットを更新する diff --git a/apps/desktop/app/hooks/projects.ts b/apps/desktop/app/hooks/projects.ts index 261b500..b5c26a0 100644 --- a/apps/desktop/app/hooks/projects.ts +++ b/apps/desktop/app/hooks/projects.ts @@ -27,7 +27,10 @@ interface ProjectKey { * @param id - プロジェクトID * @param config */ -export function useProject(id: string, config?: SWRConfiguration) { +export function useProject( + id: string, + config?: SWRConfiguration, +) { const adapter = useAdapter(); return useSWR( { type: 'project', id }, @@ -50,7 +53,9 @@ export function useCreateProject() { ); } -type UpdateProjectData = Partial>; +type UpdateProjectData = Partial< + Omit +>; /** * プロジェクトを更新する diff --git a/apps/desktop/app/hooks/settings.ts b/apps/desktop/app/hooks/settings.ts index 072f111..8ba37b6 100644 --- a/apps/desktop/app/hooks/settings.ts +++ b/apps/desktop/app/hooks/settings.ts @@ -27,8 +27,12 @@ export function useProjectSettings(projectId: string) { */ export function useUpdateProjectSettings(projectId: string) { const adapter = useAdapter(); - return useSWRMutation>( - { type: 'projectSettings', projectId }, - ({ projectId }, { arg }) => adapter.settings.update(projectId, arg), + return useSWRMutation< + ProjectSettings, + Error, + ProjectSettingKey, + Partial + >({ type: 'projectSettings', projectId }, ({ projectId }, { arg }) => + adapter.settings.update(projectId, arg), ); } diff --git a/apps/desktop/app/hooks/writings.ts b/apps/desktop/app/hooks/writings.ts index ecf47d9..645f6e2 100644 --- a/apps/desktop/app/hooks/writings.ts +++ b/apps/desktop/app/hooks/writings.ts @@ -6,15 +6,15 @@ import type { ContentItemKey, ContentTreeKey } from '~/hooks/keys'; type WritingTreeKey = ContentTreeKey<'writing'>; - /** * プロジェクト内の執筆ツリーを取得する * @param projectId - プロジェクトID */ export function useWritingTree(projectId: string) { const adapter = useAdapter(); - return useSWR({ type: 'writingTree', projectId }, ({ projectId }) => - adapter.writings.getTreeByProjectId(projectId), + return useSWR( + { type: 'writingTree', projectId }, + ({ projectId }) => adapter.writings.getTreeByProjectId(projectId), ); } @@ -25,7 +25,10 @@ type WritingKey = ContentItemKey<'writing'>; * @param id - 執筆ID * @param config */ -export function useWriting(id: string, config?: SWRConfiguration) { +export function useWriting( + id: string, + config?: SWRConfiguration, +) { const adapter = useAdapter(); return useSWR( { type: 'writing', id }, @@ -34,7 +37,10 @@ export function useWriting(id: string, config?: SWRConfiguration; +type CreateWritingData = Omit< + Writing, + 'projectId' | 'id' | 'createdAt' | 'updatedAt' +>; /** * 執筆を作成する @@ -45,14 +51,17 @@ export function useCreateWriting(projectId: string) { const adapter = useAdapter(); return useSWRMutation( { type: 'writingTree', projectId }, - ({ projectId }, { arg }) => adapter.writings.create({ - projectId, - ...arg, - }), + ({ projectId }, { arg }) => + adapter.writings.create({ + projectId, + ...arg, + }), ); } -type UpdateWritingData = Partial>; +type UpdateWritingData = Partial< + Omit +>; /** * 執筆を更新する @@ -132,7 +141,8 @@ interface TotalWordCountKey { */ export function useTotalWordCount(projectId: string) { const adapter = useAdapter(); - return useSWR({ type: 'totalWordCount', projectId }, ({ projectId }) => - adapter.writings.getTotalWordCount(projectId), + return useSWR( + { type: 'totalWordCount', projectId }, + ({ projectId }) => adapter.writings.getTotalWordCount(projectId), ); } diff --git a/apps/desktop/app/routes/(guest)/_components/feature-card.tsx b/apps/desktop/app/routes/(guest)/_components/feature-card.tsx index 090d11c..e04ab18 100644 --- a/apps/desktop/app/routes/(guest)/_components/feature-card.tsx +++ b/apps/desktop/app/routes/(guest)/_components/feature-card.tsx @@ -1,9 +1,9 @@ import * as React from 'react'; interface FeatureCardProps { - icon: React.ReactNode - title: string - description: string + icon: React.ReactNode; + title: string; + description: string; } export function FeatureCard({ icon, title, description }: FeatureCardProps) { @@ -17,5 +17,5 @@ export function FeatureCard({ icon, title, description }: FeatureCardProps) { {description}

- ) -} \ No newline at end of file + ); +} diff --git a/apps/desktop/app/routes/(guest)/layout.tsx b/apps/desktop/app/routes/(guest)/layout.tsx index 7e59319..0795936 100644 --- a/apps/desktop/app/routes/(guest)/layout.tsx +++ b/apps/desktop/app/routes/(guest)/layout.tsx @@ -14,7 +14,7 @@ export default function Layout() { return ; } // ログイン状態の場合はホームにリダイレクト - if (data && data.isAuthenticated){ + if (data && data.isAuthenticated) { return ; } diff --git a/apps/desktop/app/routes/(private)/workspace/[projectId]/_components/editors/character-editor-wrapper.tsx b/apps/desktop/app/routes/(private)/workspace/[projectId]/_components/editors/character-editor-wrapper.tsx index 579bea9..7d0bc02 100644 --- a/apps/desktop/app/routes/(private)/workspace/[projectId]/_components/editors/character-editor-wrapper.tsx +++ b/apps/desktop/app/routes/(private)/workspace/[projectId]/_components/editors/character-editor-wrapper.tsx @@ -1,10 +1,17 @@ import { useCallback } from 'react'; -import { useCharacter, useCharacterTree, useUpdateCharacter } from '~/hooks/characters'; +import { + useCharacter, + useCharacterTree, + useUpdateCharacter, +} from '~/hooks/characters'; import { CharacterEditor, type CharacterEditorData } from '@tsumugi/ui'; import { useDebouncedSave } from '~/routes/(private)/workspace/[projectId]/_hooks/useDebouncedSave'; import type { Character } from '@tsumugi/adapter'; -const NO_REVALIDATE = { revalidateOnFocus: false, revalidateOnReconnect: false } as const; +const NO_REVALIDATE = { + revalidateOnFocus: false, + revalidateOnReconnect: false, +} as const; interface CharacterEditorWrapperProps { id: string; @@ -25,29 +32,37 @@ const toEditorData = (c: Character): CharacterEditorData => ({ notes: c.notes ?? '', }); -export function CharacterEditorWrapper({ id, projectId }: CharacterEditorWrapperProps) { +export function CharacterEditorWrapper({ + id, + projectId, +}: CharacterEditorWrapperProps) { const { data: character, mutate } = useCharacter(id, NO_REVALIDATE); const { mutate: mutateTree } = useCharacterTree(projectId); const { trigger: updateCharacter } = useUpdateCharacter(id); - const onSave = useCallback(async (field: string, value: unknown) => { - await updateCharacter({ [field]: value }); - if (field === 'name') await mutateTree(); - }, [updateCharacter, mutateTree]); + const onSave = useCallback( + async (field: string, value: unknown) => { + await updateCharacter({ [field]: value }); + if (field === 'name') await mutateTree(); + }, + [updateCharacter, mutateTree], + ); const debouncedSave = useDebouncedSave(onSave); - const handleChange = useCallback((field: keyof CharacterEditorData, value: string) => { - void mutate((prev) => prev ? { ...prev, [field]: value } : prev, { revalidate: false }); - debouncedSave(field, value); - }, [mutate, debouncedSave]); + const handleChange = useCallback( + (field: keyof CharacterEditorData, value: string) => { + void mutate((prev) => (prev ? { ...prev, [field]: value } : prev), { + revalidate: false, + }); + debouncedSave(field, value); + }, + [mutate, debouncedSave], + ); if (!character) return null; return ( - + ); } diff --git a/apps/desktop/app/routes/(private)/workspace/[projectId]/_components/editors/memo-editor-wrapper.tsx b/apps/desktop/app/routes/(private)/workspace/[projectId]/_components/editors/memo-editor-wrapper.tsx index 23f85ae..74765e6 100644 --- a/apps/desktop/app/routes/(private)/workspace/[projectId]/_components/editors/memo-editor-wrapper.tsx +++ b/apps/desktop/app/routes/(private)/workspace/[projectId]/_components/editors/memo-editor-wrapper.tsx @@ -1,9 +1,16 @@ import { useCallback } from 'react'; -import { useMemo as useMemoHook, useMemoTree, useUpdateMemo } from '~/hooks/memos'; +import { + useMemo as useMemoHook, + useMemoTree, + useUpdateMemo, +} from '~/hooks/memos'; import { MemoEditor } from '@tsumugi/ui'; import { useDebouncedSave } from '~/routes/(private)/workspace/[projectId]/_hooks/useDebouncedSave'; -const NO_REVALIDATE = { revalidateOnFocus: false, revalidateOnReconnect: false } as const; +const NO_REVALIDATE = { + revalidateOnFocus: false, + revalidateOnReconnect: false, +} as const; interface MemoEditorWrapperProps { id: string; @@ -15,17 +22,25 @@ export function MemoEditorWrapper({ id, projectId }: MemoEditorWrapperProps) { const { mutate: mutateTree } = useMemoTree(projectId); const { trigger: updateMemo } = useUpdateMemo(id); - const onSave = useCallback(async (field: string, value: unknown) => { - await updateMemo({ [field]: value }); - if (field === 'name') await mutateTree(); - }, [updateMemo, mutateTree]); + const onSave = useCallback( + async (field: string, value: unknown) => { + await updateMemo({ [field]: value }); + if (field === 'name') await mutateTree(); + }, + [updateMemo, mutateTree], + ); const debouncedSave = useDebouncedSave(onSave); - const handleFieldChange = useCallback((field: string, value: unknown) => { - void mutate((prev) => prev ? { ...prev, [field]: value } : prev, { revalidate: false }); - debouncedSave(field, value); - }, [mutate, debouncedSave]); + const handleFieldChange = useCallback( + (field: string, value: unknown) => { + void mutate((prev) => (prev ? { ...prev, [field]: value } : prev), { + revalidate: false, + }); + debouncedSave(field, value); + }, + [mutate, debouncedSave], + ); if (!memo) return null; diff --git a/apps/desktop/app/routes/(private)/workspace/[projectId]/_components/editors/plot-editor-wrapper.tsx b/apps/desktop/app/routes/(private)/workspace/[projectId]/_components/editors/plot-editor-wrapper.tsx index ce2543d..ef91efd 100644 --- a/apps/desktop/app/routes/(private)/workspace/[projectId]/_components/editors/plot-editor-wrapper.tsx +++ b/apps/desktop/app/routes/(private)/workspace/[projectId]/_components/editors/plot-editor-wrapper.tsx @@ -4,7 +4,10 @@ import { PlotEditor, type PlotEditorData } from '@tsumugi/ui'; import { useDebouncedSave } from '~/routes/(private)/workspace/[projectId]/_hooks/useDebouncedSave'; import type { Plot } from '@tsumugi/adapter'; -const NO_REVALIDATE = { revalidateOnFocus: false, revalidateOnReconnect: false } as const; +const NO_REVALIDATE = { + revalidateOnFocus: false, + revalidateOnReconnect: false, +} as const; interface PlotEditorWrapperProps { id: string; @@ -27,24 +30,27 @@ export function PlotEditorWrapper({ id, projectId }: PlotEditorWrapperProps) { const { mutate: mutateTree } = usePlotTree(projectId); const { trigger: updatePlot } = useUpdatePlot(id); - const onSave = useCallback(async (field: string, value: unknown) => { - await updatePlot({ [field]: value }); - if (field === 'name') await mutateTree(); - }, [updatePlot, mutateTree]); + const onSave = useCallback( + async (field: string, value: unknown) => { + await updatePlot({ [field]: value }); + if (field === 'name') await mutateTree(); + }, + [updatePlot, mutateTree], + ); const debouncedSave = useDebouncedSave(onSave); - const handleChange = useCallback((field: keyof PlotEditorData, value: string) => { - void mutate((prev) => prev ? { ...prev, [field]: value } : prev, { revalidate: false }); - debouncedSave(field, value); - }, [mutate, debouncedSave]); + const handleChange = useCallback( + (field: keyof PlotEditorData, value: string) => { + void mutate((prev) => (prev ? { ...prev, [field]: value } : prev), { + revalidate: false, + }); + debouncedSave(field, value); + }, + [mutate, debouncedSave], + ); if (!plot) return null; - return ( - - ); + return ; } diff --git a/apps/desktop/app/routes/(private)/workspace/[projectId]/_components/editors/project-editor-wrapper.tsx b/apps/desktop/app/routes/(private)/workspace/[projectId]/_components/editors/project-editor-wrapper.tsx index a9e2c27..4143370 100644 --- a/apps/desktop/app/routes/(private)/workspace/[projectId]/_components/editors/project-editor-wrapper.tsx +++ b/apps/desktop/app/routes/(private)/workspace/[projectId]/_components/editors/project-editor-wrapper.tsx @@ -4,7 +4,10 @@ import { ProjectEditor, type ProjectEditorData } from '@tsumugi/ui'; import { useDebouncedSave } from '~/routes/(private)/workspace/[projectId]/_hooks/useDebouncedSave'; import type { Project } from '@tsumugi/adapter'; -const NO_REVALIDATE = { revalidateOnFocus: false, revalidateOnReconnect: false } as const; +const NO_REVALIDATE = { + revalidateOnFocus: false, + revalidateOnReconnect: false, +} as const; interface ProjectEditorWrapperProps { projectId: string; @@ -16,35 +19,45 @@ const toEditorData = (project: Project): ProjectEditorData => ({ synopsis: project.synopsis ?? '', theme: project.theme ?? '', goal: project.goal ?? '', - targetWordCount: project.targetWordCount != null ? String(project.targetWordCount) : '', + targetWordCount: + project.targetWordCount != null ? String(project.targetWordCount) : '', targetAudience: project.targetAudience ?? '', }); -export function ProjectEditorWrapper({ projectId, onNameChange }: ProjectEditorWrapperProps) { +export function ProjectEditorWrapper({ + projectId, + onNameChange, +}: ProjectEditorWrapperProps) { const { data: project, mutate } = useProject(projectId, NO_REVALIDATE); const { trigger: updateProject } = useUpdateProject(projectId); - const onSave = useCallback(async (field: string, value: unknown) => { - const saveValue = field === 'targetWordCount' - ? (value === '' ? undefined : Number(value)) - : value; - await updateProject({ [field]: saveValue }); - }, [updateProject]); + const onSave = useCallback( + async (field: string, value: unknown) => { + const saveValue = + field === 'targetWordCount' + ? value === '' + ? undefined + : Number(value) + : value; + await updateProject({ [field]: saveValue }); + }, + [updateProject], + ); const debouncedSave = useDebouncedSave(onSave); - const handleChange = useCallback((field: keyof ProjectEditorData, value: string) => { - void mutate((prev) => prev ? { ...prev, [field]: value } : prev, { revalidate: false }); - debouncedSave(field, value); - if (field === 'name') onNameChange?.(value); - }, [mutate, debouncedSave, onNameChange]); + const handleChange = useCallback( + (field: keyof ProjectEditorData, value: string) => { + void mutate((prev) => (prev ? { ...prev, [field]: value } : prev), { + revalidate: false, + }); + debouncedSave(field, value); + if (field === 'name') onNameChange?.(value); + }, + [mutate, debouncedSave, onNameChange], + ); if (!project) return null; - return ( - - ); + return ; } diff --git a/apps/desktop/app/routes/(private)/workspace/[projectId]/_components/editors/writing-editor-wrapper.tsx b/apps/desktop/app/routes/(private)/workspace/[projectId]/_components/editors/writing-editor-wrapper.tsx index 317678a..58b1327 100644 --- a/apps/desktop/app/routes/(private)/workspace/[projectId]/_components/editors/writing-editor-wrapper.tsx +++ b/apps/desktop/app/routes/(private)/workspace/[projectId]/_components/editors/writing-editor-wrapper.tsx @@ -3,40 +3,60 @@ import { useWriting, useWritingTree, useUpdateWriting } from '~/hooks/writings'; import { WritingEditor } from '@tsumugi/ui'; import { useDebouncedSave } from '~/routes/(private)/workspace/[projectId]/_hooks/useDebouncedSave'; -const NO_REVALIDATE = { revalidateOnFocus: false, revalidateOnReconnect: false } as const; +const NO_REVALIDATE = { + revalidateOnFocus: false, + revalidateOnReconnect: false, +} as const; interface WritingEditorWrapperProps { id: string; projectId: string; } -export function WritingEditorWrapper({ id, projectId }: WritingEditorWrapperProps) { +export function WritingEditorWrapper({ + id, + projectId, +}: WritingEditorWrapperProps) { const { data: writing, mutate } = useWriting(id, NO_REVALIDATE); const { mutate: mutateTree } = useWritingTree(projectId); const { trigger: updateWriting } = useUpdateWriting(id); - const onSave = useCallback(async (field: string, value: unknown) => { - if (field === 'content') { - const wc = (value as string).length; - await updateWriting({ content: value as string, wordCount: wc }); - } else { - await updateWriting({ [field]: value }); - } - if (field === 'name') await mutateTree(); - }, [updateWriting, mutateTree]); + const onSave = useCallback( + async (field: string, value: unknown) => { + if (field === 'content') { + const wc = (value as string).length; + await updateWriting({ content: value as string, wordCount: wc }); + } else { + await updateWriting({ [field]: value }); + } + if (field === 'name') await mutateTree(); + }, + [updateWriting, mutateTree], + ); const debouncedSave = useDebouncedSave(onSave); - const handleNameChange = useCallback((value: string) => { - void mutate((prev) => prev ? { ...prev, name: value } : prev, { revalidate: false }); - debouncedSave('name', value); - }, [mutate, debouncedSave]); + const handleNameChange = useCallback( + (value: string) => { + void mutate((prev) => (prev ? { ...prev, name: value } : prev), { + revalidate: false, + }); + debouncedSave('name', value); + }, + [mutate, debouncedSave], + ); - const handleContentChange = useCallback((value: string) => { - const wc = value.length; - void mutate((prev) => prev ? { ...prev, content: value, wordCount: wc } : prev, { revalidate: false }); - debouncedSave('content', value); - }, [mutate, debouncedSave]); + const handleContentChange = useCallback( + (value: string) => { + const wc = value.length; + void mutate( + (prev) => (prev ? { ...prev, content: value, wordCount: wc } : prev), + { revalidate: false }, + ); + debouncedSave('content', value); + }, + [mutate, debouncedSave], + ); if (!writing) return null; diff --git a/apps/desktop/app/routes/(private)/workspace/[projectId]/_components/workspace-ai-panel.tsx b/apps/desktop/app/routes/(private)/workspace/[projectId]/_components/workspace-ai-panel.tsx index ff5560d..3aa09be 100644 --- a/apps/desktop/app/routes/(private)/workspace/[projectId]/_components/workspace-ai-panel.tsx +++ b/apps/desktop/app/routes/(private)/workspace/[projectId]/_components/workspace-ai-panel.tsx @@ -6,12 +6,24 @@ import { type AiMode, type Conversation, } from '@tsumugi/ui'; -import { useAISessions, useAIMessages, useCreateAISession, useAIChat, useAcceptProposal, useRejectProposal } from '~/hooks/ai'; +import { + useAISessions, + useAIMessages, + useCreateAISession, + useAIChat, + useAcceptProposal, + useRejectProposal, +} from '~/hooks/ai'; import { useProjectSettings, useUpdateProjectSettings } from '~/hooks/settings'; import { useSWRConfig } from 'swr'; import type { AIChatContext } from '@tsumugi/adapter'; import type { EditorTab } from '@tsumugi/ui'; -import { consumeStream, toContentItemKey, toContentTreeKey, buildDisplayMessages } from '../_utils/ai-panel-utils'; +import { + consumeStream, + toContentItemKey, + toContentTreeKey, + buildDisplayMessages, +} from '../_utils/ai-panel-utils'; import { useAiStreamingState } from '../_hooks/useAiStreamingState'; interface WorkspaceAiPanelProps { @@ -38,13 +50,25 @@ function NewSessionContent({ }) { const { trigger: triggerCreateSession } = useCreateAISession(projectId); const { - isLoading, streamingContent, pendingUserMessage, streamingProposals, - setIsLoading, setStreamingContent, setPendingUserMessage, - handleToolResult, handleProposal, + isLoading, + streamingContent, + pendingUserMessage, + streamingProposals, + setIsLoading, + setStreamingContent, + setPendingUserMessage, + handleToolResult, + handleProposal, } = useAiStreamingState(projectId); const currentMessages = useMemo( - () => buildDisplayMessages(undefined, pendingUserMessage, streamingContent, streamingProposals), + () => + buildDisplayMessages( + undefined, + pendingUserMessage, + streamingContent, + streamingProposals, + ), [pendingUserMessage, streamingContent, streamingProposals], ); @@ -61,11 +85,18 @@ function NewSessionContent({ try { const result = await triggerCreateSession(request); - await consumeStream(result.stream, setStreamingContent, handleToolResult, handleProposal); + await consumeStream( + result.stream, + setStreamingContent, + handleToolResult, + handleProposal, + ); onSessionCreated(result.session.id); } catch (e) { console.error('Failed to send message:', e); - setStreamingContent(`エラーが発生しました: ${e instanceof Error ? e.message : String(e)}`); + setStreamingContent( + `エラーが発生しました: ${e instanceof Error ? e.message : String(e)}`, + ); } finally { setPendingUserMessage(null); setStreamingContent(null); @@ -86,7 +117,12 @@ function NewSessionContent({ onRejectProposal={() => {}} isLoading={isLoading} /> - + ); } @@ -113,13 +149,26 @@ function ExistingSessionContent({ const { trigger: triggerAccept } = useAcceptProposal(sessionId); const { trigger: triggerReject } = useRejectProposal(sessionId); const { - isLoading, streamingContent, pendingUserMessage, streamingProposals, - setIsLoading, setStreamingContent, setPendingUserMessage, setStreamingProposals, - handleToolResult, handleProposal, + isLoading, + streamingContent, + pendingUserMessage, + streamingProposals, + setIsLoading, + setStreamingContent, + setPendingUserMessage, + setStreamingProposals, + handleToolResult, + handleProposal, } = useAiStreamingState(projectId); const currentMessages = useMemo( - () => buildDisplayMessages(messages, pendingUserMessage, streamingContent, streamingProposals), + () => + buildDisplayMessages( + messages, + pendingUserMessage, + streamingContent, + streamingProposals, + ), [messages, pendingUserMessage, streamingContent, streamingProposals], ); @@ -127,57 +176,90 @@ function ExistingSessionContent({ * 提案を承認(サイレント) * 全提案処理済みの場合、adapter が自動で AI 応答ストリームを返すのでそれを消費する。 */ - const handleAcceptProposal = useCallback(async (proposalId: string) => { - const result = await triggerAccept(proposalId); - await mutateMessages(); - if (result.feedback.status === 'accepted' && result.feedback.contentType) { - if (result.feedback.contentType === 'project') { - void globalMutate({ type: 'project', id: projectId }); - } else { - if (result.feedback.targetId) { - void globalMutate(toContentItemKey(result.feedback.contentType, result.feedback.targetId)); + const handleAcceptProposal = useCallback( + async (proposalId: string) => { + const result = await triggerAccept(proposalId); + await mutateMessages(); + if ( + result.feedback.status === 'accepted' && + result.feedback.contentType + ) { + if (result.feedback.contentType === 'project') { + void globalMutate({ type: 'project', id: projectId }); + } else { + if (result.feedback.targetId) { + void globalMutate( + toContentItemKey( + result.feedback.contentType, + result.feedback.targetId, + ), + ); + } + void globalMutate( + toContentTreeKey(result.feedback.contentType, projectId), + ); } - void globalMutate(toContentTreeKey(result.feedback.contentType, projectId)); } - } - if (result.stream) { - setIsLoading(true); - setStreamingContent(''); - try { - await consumeStream(result.stream, setStreamingContent, handleToolResult, handleProposal); - await mutateMessages(); - setStreamingProposals([]); - } catch (e) { - console.error('Failed to consume auto feedback stream:', e); - } finally { - setStreamingContent(null); - setIsLoading(false); + if (result.stream) { + setIsLoading(true); + setStreamingContent(''); + try { + await consumeStream( + result.stream, + setStreamingContent, + handleToolResult, + handleProposal, + ); + await mutateMessages(); + setStreamingProposals([]); + } catch (e) { + console.error('Failed to consume auto feedback stream:', e); + } finally { + setStreamingContent(null); + setIsLoading(false); + } } - } - }, [triggerAccept, mutateMessages, globalMutate, projectId, handleToolResult, handleProposal]); + }, + [ + triggerAccept, + mutateMessages, + globalMutate, + projectId, + handleToolResult, + handleProposal, + ], + ); /** * 提案を拒否(サイレント) * 全提案処理済みの場合、adapter が自動で AI 応答ストリームを返すのでそれを消費する。 */ - const handleRejectProposal = useCallback(async (proposalId: string) => { - const result = await triggerReject(proposalId); - await mutateMessages(); - if (result.stream) { - setIsLoading(true); - setStreamingContent(''); - try { - await consumeStream(result.stream, setStreamingContent, handleToolResult, handleProposal); - await mutateMessages(); - setStreamingProposals([]); - } catch (e) { - console.error('Failed to consume auto feedback stream:', e); - } finally { - setStreamingContent(null); - setIsLoading(false); + const handleRejectProposal = useCallback( + async (proposalId: string) => { + const result = await triggerReject(proposalId); + await mutateMessages(); + if (result.stream) { + setIsLoading(true); + setStreamingContent(''); + try { + await consumeStream( + result.stream, + setStreamingContent, + handleToolResult, + handleProposal, + ); + await mutateMessages(); + setStreamingProposals([]); + } catch (e) { + console.error('Failed to consume auto feedback stream:', e); + } finally { + setStreamingContent(null); + setIsLoading(false); + } } - } - }, [triggerReject, mutateMessages, handleToolResult, handleProposal]); + }, + [triggerReject, mutateMessages, handleToolResult, handleProposal], + ); const handleSend = async (message: string) => { const request = { @@ -192,12 +274,19 @@ function ExistingSessionContent({ try { const stream = await triggerChat(request); - await consumeStream(stream, setStreamingContent, handleToolResult, handleProposal); + await consumeStream( + stream, + setStreamingContent, + handleToolResult, + handleProposal, + ); await mutateMessages(); setStreamingProposals([]); } catch (e) { console.error('Failed to send message:', e); - setStreamingContent(`エラーが発生しました: ${e instanceof Error ? e.message : String(e)}`); + setStreamingContent( + `エラーが発生しました: ${e instanceof Error ? e.message : String(e)}`, + ); } finally { setPendingUserMessage(null); setStreamingContent(null); @@ -218,16 +307,27 @@ function ExistingSessionContent({ onRejectProposal={handleRejectProposal} isLoading={isLoading} /> - + ); } -export function WorkspaceAiPanel({ projectId, openTabs }: WorkspaceAiPanelProps) { +export function WorkspaceAiPanel({ + projectId, + openTabs, +}: WorkspaceAiPanelProps) { const { data: sessions } = useAISessions(projectId); const { data: settings } = useProjectSettings(projectId); - const { trigger: triggerUpdateSettings } = useUpdateProjectSettings(projectId); - const [currentConversationId, setCurrentConversationId] = useState(); + const { trigger: triggerUpdateSettings } = + useUpdateProjectSettings(projectId); + const [currentConversationId, setCurrentConversationId] = useState< + string | undefined + >(); const aiMode: AiMode = settings?.aiChatMode ?? 'ask'; const handleModeChange = useCallback( @@ -263,7 +363,12 @@ export function WorkspaceAiPanel({ projectId, openTabs }: WorkspaceAiPanelProps) context={ openTabs?.length ? { - openTabs: openTabs.map((t) => ({ id: t.id, name: t.name, contentType: t.type, ...(t.active ? { active: true } : {}) })), + openTabs: openTabs.map((t) => ({ + id: t.id, + name: t.name, + contentType: t.type, + ...(t.active ? { active: true } : {}), + })), } : undefined } @@ -277,7 +382,12 @@ export function WorkspaceAiPanel({ projectId, openTabs }: WorkspaceAiPanelProps) context={ openTabs?.length ? { - openTabs: openTabs.map((t) => ({ id: t.id, name: t.name, contentType: t.type, ...(t.active ? { active: true } : {}) })), + openTabs: openTabs.map((t) => ({ + id: t.id, + name: t.name, + contentType: t.type, + ...(t.active ? { active: true } : {}), + })), } : undefined } diff --git a/apps/desktop/app/routes/(private)/workspace/[projectId]/_components/workspace-editor.tsx b/apps/desktop/app/routes/(private)/workspace/[projectId]/_components/workspace-editor.tsx index e5a5fd2..143c386 100644 --- a/apps/desktop/app/routes/(private)/workspace/[projectId]/_components/workspace-editor.tsx +++ b/apps/desktop/app/routes/(private)/workspace/[projectId]/_components/workspace-editor.tsx @@ -11,34 +11,51 @@ interface WorkspaceEditorProps { onProjectNameChange?: (name: string) => void; } -export function WorkspaceEditor({ projectId, selectedNode, onProjectNameChange }: WorkspaceEditorProps) { +export function WorkspaceEditor({ + projectId, + selectedNode, + onProjectNameChange, +}: WorkspaceEditorProps) { if (!selectedNode) { return (
-

左のサイドバーからファイルを選択してください

+

+ 左のサイドバーからファイルを選択してください +

); } if (selectedNode.type === 'project') { - return ; + return ( + + ); } if (selectedNode.nodeType !== 'file') { return (
-

左のサイドバーからファイルを選択してください

+

+ 左のサイドバーからファイルを選択してください +

); } switch (selectedNode.type) { case 'writing': - return ; + return ( + + ); case 'plot': return ; case 'character': - return ; + return ( + + ); case 'memo': return ; } diff --git a/apps/desktop/app/routes/(private)/workspace/[projectId]/_components/workspace-sidebar.tsx b/apps/desktop/app/routes/(private)/workspace/[projectId]/_components/workspace-sidebar.tsx index 3e0a570..5ac3237 100644 --- a/apps/desktop/app/routes/(private)/workspace/[projectId]/_components/workspace-sidebar.tsx +++ b/apps/desktop/app/routes/(private)/workspace/[projectId]/_components/workspace-sidebar.tsx @@ -1,9 +1,29 @@ import type { TreeNode } from '@tsumugi/adapter'; import { useNavigate } from 'react-router'; -import { useWritingTree, useCreateWriting, useDeleteWritingFromTree, useReorderWritings } from '~/hooks/writings'; -import { usePlotTree, useCreatePlot, useDeletePlotFromTree, useReorderPlots } from '~/hooks/plots'; -import { useCharacterTree, useCreateCharacter, useDeleteCharacterFromTree, useReorderCharacters } from '~/hooks/characters'; -import { useMemoTree, useCreateMemo, useDeleteMemoFromTree, useReorderMemos } from '~/hooks/memos'; +import { + useWritingTree, + useCreateWriting, + useDeleteWritingFromTree, + useReorderWritings, +} from '~/hooks/writings'; +import { + usePlotTree, + useCreatePlot, + useDeletePlotFromTree, + useReorderPlots, +} from '~/hooks/plots'; +import { + useCharacterTree, + useCreateCharacter, + useDeleteCharacterFromTree, + useReorderCharacters, +} from '~/hooks/characters'; +import { + useMemoTree, + useCreateMemo, + useDeleteMemoFromTree, + useReorderMemos, +} from '~/hooks/memos'; import { Sidebar, SidebarSection, @@ -14,7 +34,10 @@ import { import { ArrowLeftIcon, SettingsIcon } from 'lucide-react'; import { PATH_HOME } from '~/constants/path'; -function convertTreeNodes(nodes: TreeNode[], type: ContentType): TreeNodeData[] { +function convertTreeNodes( + nodes: TreeNode[], + type: ContentType, +): TreeNodeData[] { return nodes.map((node) => ({ id: node.id, name: node.name, @@ -25,7 +48,10 @@ function convertTreeNodes(nodes: TreeNode[], type: ContentType): TreeNodeData[] } function flattenTree(nodes: TreeNode[]): TreeNode[] { - return nodes.flatMap((n) => [n, ...(n.children ? flattenTree(n.children) : [])]); + return nodes.flatMap((n) => [ + n, + ...(n.children ? flattenTree(n.children) : []), + ]); } // ─── 汎用セクションコンポーネント ─── @@ -35,9 +61,14 @@ interface ContentSectionProps { defaultName: string; defaultFields?: Record; tree: TreeNode[] | undefined; - create: (data: Record) => Promise<{ id: string; name: string; nodeType: string }>; + create: ( + data: Record, + ) => Promise<{ id: string; name: string; nodeType: string }>; remove: (id: string) => Promise; - reorder: (arg: { parentId: string | null; orderedIds: string[] }) => Promise; + reorder: (arg: { + parentId: string | null; + orderedIds: string[]; + }) => Promise; selectedNodeId: string | null; onSelectNode: (node: TreeNodeData) => void; onDeselectNode: () => void; @@ -61,9 +92,18 @@ function ContentSection({ const newOrder = flattenTree(tree ?? []).length; try { const item = await create({ - parentId, name: defaultName, nodeType: type, order: newOrder, ...defaultFields, + parentId, + name: defaultName, + nodeType: type, + order: newOrder, + ...defaultFields, + }); + onSelectNode({ + id: item.id, + name: item.name, + type, + nodeType: item.nodeType === 'folder' ? 'folder' : 'file', }); - onSelectNode({ id: item.id, name: item.name, type, nodeType: item.nodeType === 'folder' ? 'folder' : 'file' }); } catch (e) { console.error(`Failed to create ${type}:`, e); } @@ -78,7 +118,10 @@ function ContentSection({ } }; - const handleReorder = async (parentId: string | null, orderedIds: string[]) => { + const handleReorder = async ( + parentId: string | null, + orderedIds: string[], + ) => { try { await reorder({ parentId, orderedIds }); } catch (e) { @@ -86,7 +129,16 @@ function ContentSection({ } }; - return ; + return ( + + ); } // ─── 各カテゴリ(hooks をバインド) ─── @@ -103,8 +155,19 @@ function PlotSection({ projectId, ...rest }: CategoryProps) { const { trigger: triggerCreate } = useCreatePlot(projectId); const { trigger: remove } = useDeletePlotFromTree(projectId); const { trigger: reorder } = useReorderPlots(projectId); - const create = (data: Record) => triggerCreate(data as Parameters[0]); - return ; + const create = (data: Record) => + triggerCreate(data as Parameters[0]); + return ( + + ); } function CharacterSection({ projectId, ...rest }: CategoryProps) { @@ -112,8 +175,19 @@ function CharacterSection({ projectId, ...rest }: CategoryProps) { const { trigger: triggerCreate } = useCreateCharacter(projectId); const { trigger: remove } = useDeleteCharacterFromTree(projectId); const { trigger: reorder } = useReorderCharacters(projectId); - const create = (data: Record) => triggerCreate(data as Parameters[0]); - return ; + const create = (data: Record) => + triggerCreate(data as Parameters[0]); + return ( + + ); } function MemoSection({ projectId, ...rest }: CategoryProps) { @@ -121,8 +195,20 @@ function MemoSection({ projectId, ...rest }: CategoryProps) { const { trigger: triggerCreate } = useCreateMemo(projectId); const { trigger: remove } = useDeleteMemoFromTree(projectId); const { trigger: reorder } = useReorderMemos(projectId); - const create = (data: Record) => triggerCreate(data as Parameters[0]); - return ; + const create = (data: Record) => + triggerCreate(data as Parameters[0]); + return ( + + ); } function WritingSection({ projectId, ...rest }: CategoryProps) { @@ -130,8 +216,20 @@ function WritingSection({ projectId, ...rest }: CategoryProps) { const { trigger: triggerCreate } = useCreateWriting(projectId); const { trigger: remove } = useDeleteWritingFromTree(projectId); const { trigger: reorder } = useReorderWritings(projectId); - const create = (data: Record) => triggerCreate(data as Parameters[0]); - return ; + const create = (data: Record) => + triggerCreate(data as Parameters[0]); + return ( + + ); } // ─── WorkspaceSidebar ─── @@ -168,7 +266,9 @@ export function WorkspaceSidebar({ > - {projectName} + + {projectName} + @@ -111,12 +134,18 @@ function ConversationSelector({ filteredConversations?.map((conversation) => ( - )} - ) + ); } function MessageBubble({ @@ -322,12 +356,12 @@ function MessageBubble({ onRejectProposal, isLoading = false, }: { - message: Message - onAcceptProposal?: (proposalId: string) => void - onRejectProposal?: (proposalId: string) => void - isLoading?: boolean + message: Message; + onAcceptProposal?: (proposalId: string) => void; + onRejectProposal?: (proposalId: string) => void; + isLoading?: boolean; }) { - if ("proposal" in message) { + if ('proposal' in message) { return (
- ) + ); } - const isUser = message.role === "user" + const isUser = message.role === 'user'; return (
{isUser ? (

{message.content}

@@ -357,47 +386,48 @@ function MessageBubble({ {message.content} )}
- ) + ); } /** * 最下部にいるときだけ自動スクロールするフック */ function useAutoScroll(deps: React.DependencyList) { - const viewportRef = React.useRef(null) - const isAtBottomRef = React.useRef(true) + const viewportRef = React.useRef(null); + const isAtBottomRef = React.useRef(true); - const THRESHOLD = 30 + const THRESHOLD = 30; const handleScroll = React.useCallback(() => { - const el = viewportRef.current - if (!el) return - isAtBottomRef.current = el.scrollHeight - el.scrollTop - el.clientHeight <= THRESHOLD - }, []) + const el = viewportRef.current; + if (!el) return; + isAtBottomRef.current = + el.scrollHeight - el.scrollTop - el.clientHeight <= THRESHOLD; + }, []); React.useEffect(() => { - const el = viewportRef.current - if (!el) return - el.addEventListener("scroll", handleScroll, { passive: true }) - return () => el.removeEventListener("scroll", handleScroll) - }, [handleScroll]) + const el = viewportRef.current; + if (!el) return; + el.addEventListener('scroll', handleScroll, { passive: true }); + return () => el.removeEventListener('scroll', handleScroll); + }, [handleScroll]); React.useEffect(() => { - const el = viewportRef.current + const el = viewportRef.current; if (el && isAtBottomRef.current) { - el.scrollTop = el.scrollHeight + el.scrollTop = el.scrollHeight; } - }, deps) + }, deps); const scrollToBottom = React.useCallback(() => { - const el = viewportRef.current + const el = viewportRef.current; if (el) { - el.scrollTop = el.scrollHeight - isAtBottomRef.current = true + el.scrollTop = el.scrollHeight; + isAtBottomRef.current = true; } - }, []) + }, []); - return { viewportRef, scrollToBottom } + return { viewportRef, scrollToBottom }; } export function AiPanelContent({ @@ -407,16 +437,16 @@ export function AiPanelContent({ onRejectProposal, isLoading = false, }: AiPanelContentProps) { - const { viewportRef, scrollToBottom } = useAutoScroll([messages, isLoading]) + const { viewportRef, scrollToBottom } = useAutoScroll([messages, isLoading]); // 送信時(メッセージ数が増えたとき)は強制スクロール - const prevMessageCountRef = React.useRef(messages.length) + const prevMessageCountRef = React.useRef(messages.length); React.useEffect(() => { if (messages.length > prevMessageCountRef.current) { - scrollToBottom() + scrollToBottom(); } - prevMessageCountRef.current = messages.length - }, [messages.length, scrollToBottom]) + prevMessageCountRef.current = messages.length; + }, [messages.length, scrollToBottom]); return ( @@ -424,7 +454,9 @@ export function AiPanelContent({ {messages.length === 0 ? (

始めましょう

- {description &&

{description}

} + {description && ( +

{description}

+ )}
) : ( messages.map((message) => ( @@ -444,24 +476,24 @@ export function AiPanelContent({ )}
- ) + ); } // --- AiPanelInput (入力欄 + モード切替) --- export interface AiPanelInputProps { - mode: AiMode - onModeChange?: (mode: AiMode) => void - onSend?: (message: string) => void - isLoading?: boolean + mode: AiMode; + onModeChange?: (mode: AiMode) => void; + onSend?: (message: string) => void; + isLoading?: boolean; } function ModeToggleFooter({ mode, onModeChange, }: { - mode: AiMode - onModeChange?: (mode: AiMode) => void + mode: AiMode; + onModeChange?: (mode: AiMode) => void; }) { return ( @@ -471,7 +503,7 @@ function ModeToggleFooter({ size="sm" className="h-8 px-2 text-xs font-medium" > - {mode === "write" ? ( + {mode === 'write' ? ( <> Write @@ -489,19 +521,19 @@ function ModeToggleFooter({
- ) + ); } export function AiPanelInput({ @@ -518,15 +550,15 @@ export function AiPanelInput({ onSend, isLoading = false, }: AiPanelInputProps) { - const [input, setInput] = React.useState("") + const [input, setInput] = React.useState(''); const handleSubmit = (e: React.FormEvent) => { - e.preventDefault() + e.preventDefault(); if (input.trim() && !isLoading) { - onSend?.(input.trim()) - setInput("") + onSend?.(input.trim()); + setInput(''); } - } + }; return (
@@ -535,16 +567,14 @@ export function AiPanelInput({ value={input} onChange={(e) => setInput(e.target.value)} placeholder={ - mode === "write" - ? "どのような文章を書きますか?" - : "質問を入力..." + mode === 'write' ? 'どのような文章を書きますか?' : '質問を入力...' } className="resize-none border-0 rounded-t-lg rounded-b-none focus-visible:ring-0 focus-visible:ring-offset-0 min-h-[80px]" rows={3} onKeyDown={(e) => { - if (e.key === "Enter" && !e.shiftKey && !isIMEActive(e)) { - e.preventDefault() - handleSubmit(e) + if (e.key === 'Enter' && !e.shiftKey && !isIMEActive(e)) { + e.preventDefault(); + handleSubmit(e); } }} /> @@ -562,6 +592,5 @@ export function AiPanelInput({
- ) + ); } - diff --git a/packages/ui/src/components/features/ai-panel/index.ts b/packages/ui/src/components/features/ai-panel/index.ts index b39b015..53db4f6 100644 --- a/packages/ui/src/components/features/ai-panel/index.ts +++ b/packages/ui/src/components/features/ai-panel/index.ts @@ -1 +1 @@ -export * from "./ai-panel"; +export * from './ai-panel'; diff --git a/packages/ui/src/components/features/editor/character-editor.stories.tsx b/packages/ui/src/components/features/editor/character-editor.stories.tsx index f8264cd..8b01b11 100644 --- a/packages/ui/src/components/features/editor/character-editor.stories.tsx +++ b/packages/ui/src/components/features/editor/character-editor.stories.tsx @@ -1,17 +1,17 @@ -import type { Meta, StoryObj } from "@storybook/react"; -import { useState } from "react"; -import { CharacterEditor, type CharacterEditorData } from "./character-editor"; +import type { Meta, StoryObj } from '@storybook/react'; +import { useState } from 'react'; +import { CharacterEditor, type CharacterEditorData } from './character-editor'; const meta = { - title: "Features/CharacterEditor", + title: 'Features/CharacterEditor', component: CharacterEditor, parameters: { - layout: "fullscreen", + layout: 'fullscreen', }, - tags: ["autodocs"], + tags: ['autodocs'], decorators: [ (Story) => ( -
+
), @@ -22,17 +22,20 @@ export default meta; type Story = StoryObj; const mockData: CharacterEditorData = { - name: "田中太郎", - aliases: "タロー、勇者", - role: "主人公", - gender: "男性", - age: "18歳", - appearance: "黒髪短髪。身長175cm。鍛えられた体つきだが、目元は柔和。", - personality: "正義感が強く、困っている人を放っておけない。やや無鉄砲なところがある。", - background: "小さな村で育った農家の息子。幼い頃に両親を亡くし、祖父に育てられた。", - motivation: "祖父の病を治す薬草を求めて旅に出る。", - relationships: "幼馴染のヒロインとは兄妹のような関係。師匠の老騎士を尊敬している。", - notes: "第3章で覚醒イベントあり。伏線を第1章から張っておく。", + name: '田中太郎', + aliases: 'タロー、勇者', + role: '主人公', + gender: '男性', + age: '18歳', + appearance: '黒髪短髪。身長175cm。鍛えられた体つきだが、目元は柔和。', + personality: + '正義感が強く、困っている人を放っておけない。やや無鉄砲なところがある。', + background: + '小さな村で育った農家の息子。幼い頃に両親を亡くし、祖父に育てられた。', + motivation: '祖父の病を治す薬草を求めて旅に出る。', + relationships: + '幼馴染のヒロインとは兄妹のような関係。師匠の老騎士を尊敬している。', + notes: '第3章で覚醒イベントあり。伏線を第1章から張っておく。', }; export const Default: Story = { @@ -43,7 +46,7 @@ export const Default: Story = { export const Empty: Story = { args: { - data: { name: "新しいキャラクター" }, + data: { name: '新しいキャラクター' }, }, }; diff --git a/packages/ui/src/components/features/editor/character-editor.tsx b/packages/ui/src/components/features/editor/character-editor.tsx index 44228a7..499526d 100644 --- a/packages/ui/src/components/features/editor/character-editor.tsx +++ b/packages/ui/src/components/features/editor/character-editor.tsx @@ -1,44 +1,44 @@ -import * as React from "react" -import { ScrollArea } from "@/components/ui/scroll-area" -import { Textarea } from "@/components/ui/textarea" -import { cn } from "@/lib/utils" +import * as React from 'react'; +import { ScrollArea } from '@/components/ui/scroll-area'; +import { Textarea } from '@/components/ui/textarea'; +import { cn } from '@/lib/utils'; export interface CharacterEditorData { - name?: string - aliases?: string - role?: string - gender?: string - age?: string - appearance?: string - personality?: string - background?: string - motivation?: string - relationships?: string - notes?: string + name?: string; + aliases?: string; + role?: string; + gender?: string; + age?: string; + appearance?: string; + personality?: string; + background?: string; + motivation?: string; + relationships?: string; + notes?: string; } export interface CharacterEditorProps { - data: CharacterEditorData - onChange?: (field: keyof CharacterEditorData, value: string) => void - className?: string - readOnly?: boolean + data: CharacterEditorData; + onChange?: (field: keyof CharacterEditorData, value: string) => void; + className?: string; + readOnly?: boolean; } const shortFields: { key: keyof CharacterEditorData; label: string }[] = [ - { key: "aliases", label: "別名・あだ名" }, - { key: "role", label: "役割" }, - { key: "gender", label: "性別" }, - { key: "age", label: "年齢" }, -] + { key: 'aliases', label: '別名・あだ名' }, + { key: 'role', label: '役割' }, + { key: 'gender', label: '性別' }, + { key: 'age', label: '年齢' }, +]; const longFields: { key: keyof CharacterEditorData; label: string }[] = [ - { key: "appearance", label: "外見" }, - { key: "personality", label: "性格" }, - { key: "background", label: "経歴・背景" }, - { key: "motivation", label: "動機・目的" }, - { key: "relationships", label: "人間関係" }, - { key: "notes", label: "メモ" }, -] + { key: 'appearance', label: '外見' }, + { key: 'personality', label: '性格' }, + { key: 'background', label: '経歴・背景' }, + { key: 'motivation', label: '動機・目的' }, + { key: 'relationships', label: '人間関係' }, + { key: 'notes', label: 'メモ' }, +]; export function CharacterEditor({ data, @@ -47,14 +47,14 @@ export function CharacterEditor({ readOnly = false, }: CharacterEditorProps) { return ( -
+
{data.name !== undefined && (
{onChange ? ( onChange("name", e.target.value)} + onChange={(e) => onChange('name', e.target.value)} className="w-full bg-transparent text-lg font-semibold outline-none focus:ring-1 focus:ring-ring rounded px-1" placeholder="キャラクター名を入力..." readOnly={readOnly} @@ -74,14 +74,14 @@ export function CharacterEditor({ onChange?.(key, e.target.value)} readOnly={readOnly} className={cn( - "w-full rounded-md border bg-transparent px-3 py-2 text-sm outline-none", - "focus:ring-1 focus:ring-ring", - "placeholder:text-muted-foreground", - readOnly && "cursor-default" + 'w-full rounded-md border bg-transparent px-3 py-2 text-sm outline-none', + 'focus:ring-1 focus:ring-ring', + 'placeholder:text-muted-foreground', + readOnly && 'cursor-default', )} placeholder={`${label}を入力...`} /> @@ -95,13 +95,10 @@ export function CharacterEditor({ {label}