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}
+