mirror of
https://github.com/tiddly-gittly/TidGi-Desktop.git
synced 2025-12-06 02:30:47 -08:00
feat: create new chat in the store and wiki
This commit is contained in:
parent
45d12ce1a0
commit
083eda4e6d
9 changed files with 277 additions and 52 deletions
|
|
@ -488,7 +488,9 @@
|
|||
"InPort": "In Ports",
|
||||
"StopWorkflow": "Stop Workflow",
|
||||
"ToggleDebugPanel": "Toggle Debug Panel",
|
||||
"ClearDebugPanel": "Clear Debug Panel"
|
||||
"ClearDebugPanel": "Clear Debug Panel",
|
||||
"NewChat": "New Chat",
|
||||
"DeleteChat": "Delete Chat"
|
||||
},
|
||||
"Description": "Description",
|
||||
"Tags": "Tags",
|
||||
|
|
|
|||
|
|
@ -493,6 +493,8 @@
|
|||
"InPort": "入口",
|
||||
"StopWorkflow": "停止工作流",
|
||||
"ToggleDebugPanel": "切换开关调试面板",
|
||||
"ClearDebugPanel": "清空调试面板"
|
||||
"ClearDebugPanel": "清空调试面板",
|
||||
"NewChat": "新对话",
|
||||
"DeleteChat": "删除对话"
|
||||
}
|
||||
}
|
||||
|
|
|
|||
85
src/pages/Workflow/RunWorkflow/ChatArea.tsx
Normal file
85
src/pages/Workflow/RunWorkflow/ChatArea.tsx
Normal file
|
|
@ -0,0 +1,85 @@
|
|||
import ChatBubbleOutline from '@mui/icons-material/ChatBubbleOutline';
|
||||
import { Box, IconButton, Typography } from '@mui/material';
|
||||
import React, { useRef } from 'react';
|
||||
import { styled } from 'styled-components';
|
||||
import { plugins } from '../DebugPanel/plugins';
|
||||
import { useChatsStore } from './useChatsStore';
|
||||
|
||||
const Container = styled.div`
|
||||
padding: 0 1em;
|
||||
height: 100%;
|
||||
overflow-y: auto;
|
||||
&::-webkit-scrollbar {
|
||||
width: 0;
|
||||
}
|
||||
`;
|
||||
|
||||
interface Props {
|
||||
chatID: string;
|
||||
}
|
||||
|
||||
export const ChatArea: React.FC<Props> = ({ chatID }) => {
|
||||
/**
|
||||
* Use the chatsStore to get the relevant chat elements.
|
||||
*/
|
||||
const elements = useChatsStore((state) => state.chats[chatID]?.chatJSON?.elements ?? {});
|
||||
|
||||
/**
|
||||
* Ref to the Container element for scrolling
|
||||
*/
|
||||
const containerReference = useRef<HTMLDivElement | null>(null);
|
||||
|
||||
const onSubmit = useChatsStore((state) => (id: string, content: unknown) => {
|
||||
state.submitElementInChat(chatID, id, content);
|
||||
});
|
||||
|
||||
return (
|
||||
<Container ref={containerReference}>
|
||||
{Object.values(elements).map(element => {
|
||||
// eslint-disable-next-line unicorn/no-null, @typescript-eslint/strict-boolean-expressions
|
||||
if (!element) return null;
|
||||
|
||||
const { type, id, props = {}, isSubmitted, timestamp } = element;
|
||||
const plugin = plugins.find(p => p.type === type);
|
||||
if (plugin === undefined) {
|
||||
// TODO: return a placeholder element instead
|
||||
// eslint-disable-next-line unicorn/no-null
|
||||
return null;
|
||||
}
|
||||
const { Component } = plugin;
|
||||
return (
|
||||
<div key={id}>
|
||||
<Typography color='textSecondary'>
|
||||
{new Date(timestamp).toLocaleTimeString()}
|
||||
</Typography>
|
||||
<Component {...props} onSubmit={onSubmit} id={id} isSubmitted={isSubmitted} />
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</Container>
|
||||
);
|
||||
};
|
||||
|
||||
const EmptyContainer = styled(Box)`
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
height: 100%;
|
||||
opacity: 0.7;
|
||||
`;
|
||||
export const EmptyChatArea: React.FC = () => {
|
||||
return (
|
||||
<EmptyContainer>
|
||||
<IconButton color='primary' size='large'>
|
||||
<ChatBubbleOutline fontSize='inherit' />
|
||||
</IconButton>
|
||||
<Typography variant='h6' gutterBottom>
|
||||
No Chat Selected
|
||||
</Typography>
|
||||
<Typography color='textSecondary'>
|
||||
Start a chat by sending a message.
|
||||
</Typography>
|
||||
</EmptyContainer>
|
||||
);
|
||||
};
|
||||
56
src/pages/Workflow/RunWorkflow/ChatsList.tsx
Normal file
56
src/pages/Workflow/RunWorkflow/ChatsList.tsx
Normal file
|
|
@ -0,0 +1,56 @@
|
|||
/* eslint-disable @typescript-eslint/promise-function-async */
|
||||
import { Button, List, ListItem } from '@mui/material';
|
||||
import { IWorkspaceWithMetadata } from '@services/workspaces/interface';
|
||||
import React from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { styled } from 'styled-components';
|
||||
import { useChatDataSource } from './useChatDataSource';
|
||||
|
||||
// Styled Components
|
||||
const Container = styled.div`
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
`;
|
||||
|
||||
const AddChatButton = styled(Button)`
|
||||
margin: 10px 0;
|
||||
`;
|
||||
|
||||
const StyledListItem = styled(ListItem)`
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
`;
|
||||
|
||||
interface IChatsListProps {
|
||||
workflowID: string | undefined;
|
||||
workspacesList: IWorkspaceWithMetadata[] | undefined;
|
||||
}
|
||||
|
||||
export const ChatsList: React.FC<IChatsListProps> = ({ workflowID, workspacesList }) => {
|
||||
const { t } = useTranslation();
|
||||
const [chatList, onAddChat, onDeleteChat] = useChatDataSource(workspacesList, workflowID);
|
||||
|
||||
return (
|
||||
<Container>
|
||||
<AddChatButton variant='contained' color='primary' onClick={() => onAddChat()}>
|
||||
{t('Workflow.NewChat')}
|
||||
</AddChatButton>
|
||||
<List>
|
||||
{chatList.map((chat) => (
|
||||
<StyledListItem key={chat.id}>
|
||||
{chat.title}
|
||||
<Button
|
||||
onClick={() => {
|
||||
onDeleteChat(chat.id);
|
||||
}}
|
||||
>
|
||||
{t('Workflow.DeleteChat')}
|
||||
</Button>
|
||||
</StyledListItem>
|
||||
))}
|
||||
</List>
|
||||
</Container>
|
||||
);
|
||||
};
|
||||
33
src/pages/Workflow/RunWorkflow/index.tsx
Normal file
33
src/pages/Workflow/RunWorkflow/index.tsx
Normal file
|
|
@ -0,0 +1,33 @@
|
|||
import { PageType } from '@services/pages/interface';
|
||||
import { WindowNames } from '@services/windows/WindowProperties';
|
||||
import { useWorkspacesListObservable } from '@services/workspaces/hooks';
|
||||
import React from 'react';
|
||||
import { styled } from 'styled-components';
|
||||
import { useRoute } from 'wouter';
|
||||
import { ChatArea, EmptyChatArea } from './ChatArea';
|
||||
import { ChatsList } from './ChatsList';
|
||||
|
||||
const Container = styled.div`
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 1rem;
|
||||
`;
|
||||
|
||||
export const RunWorkflow: React.FC = () => {
|
||||
const [, parameters] = useRoute(`/${WindowNames.main}/${PageType.workflow}/run/:workflowID/:runID/`);
|
||||
/**
|
||||
* This will be the active chat ID if the URL matches, otherwise it'll be undefined.
|
||||
*/
|
||||
const activeRunID = parameters?.runID;
|
||||
const workflowID = parameters?.workflowID;
|
||||
const workspacesList = useWorkspacesListObservable();
|
||||
// DEBUG: console workflowID
|
||||
console.log(`workflowID`, workflowID, parameters);
|
||||
|
||||
return (
|
||||
<Container>
|
||||
<ChatsList workflowID={workflowID} workspacesList={workspacesList} />
|
||||
{activeRunID === undefined ? <EmptyChatArea /> : <ChatArea chatID={activeRunID} />}
|
||||
</Container>
|
||||
);
|
||||
};
|
||||
|
|
@ -8,9 +8,10 @@ export interface ChatsStoreState {
|
|||
chats: Record<string, IChatListItem | undefined>;
|
||||
}
|
||||
export interface ChatsStoreActions {
|
||||
addChat: (fields: { title?: string; workflowID: string; workspaceID: string }) => string;
|
||||
addChat: (fields: { title?: string; workflowID: string; workspaceID: string }) => IChatListItem;
|
||||
addElementToChat: (chatID: string, element: Pick<UIElementState, 'type' | 'props' | 'author'>) => string;
|
||||
clearElementsInChat: (chatID: string) => void;
|
||||
getChat: (chatID: string) => IChatListItem | undefined;
|
||||
removeChat: (chatID: string) => void;
|
||||
removeElementFromChat: (chatID: string, id: string) => void;
|
||||
submitElementInChat: (chatID: string, id: string, content: unknown) => void;
|
||||
|
|
@ -28,12 +29,21 @@ export const chatsStore = createStore(
|
|||
});
|
||||
},
|
||||
|
||||
getChat: (chatID) => {
|
||||
let result;
|
||||
set((state) => {
|
||||
result = state.chats[chatID];
|
||||
});
|
||||
return result;
|
||||
},
|
||||
|
||||
addChat: (fields) => {
|
||||
const id = String(Math.random());
|
||||
const newChatItem = { chatJSON: { elements: {} }, id, tags: [], title: 'New Chat', ...fields };
|
||||
set((state) => {
|
||||
state.chats[id] = { chatJSON: { elements: {} }, id, tags: [], title: 'New Chat', ...fields };
|
||||
state.chats[id] = newChatItem;
|
||||
});
|
||||
return id;
|
||||
return newChatItem;
|
||||
},
|
||||
|
||||
addElementToChat: (chatID, { type, props, author }) => {
|
||||
|
|
|
|||
|
|
@ -28,6 +28,9 @@ export interface IChatListItem {
|
|||
*/
|
||||
chatJSONString?: string;
|
||||
description?: string;
|
||||
/**
|
||||
* Random generated ID
|
||||
*/
|
||||
id: string;
|
||||
image?: string;
|
||||
metadata?: {
|
||||
|
|
@ -35,6 +38,9 @@ export interface IChatListItem {
|
|||
workspace: IWorkspaceWithMetadata;
|
||||
};
|
||||
tags: string[];
|
||||
/**
|
||||
* From caption field, or use ID
|
||||
*/
|
||||
title: string;
|
||||
workflowID: string;
|
||||
workspaceID: string;
|
||||
|
|
@ -67,7 +73,7 @@ export function useChatsFromWiki(workspacesList: IWorkspaceWithMetadata[] | unde
|
|||
const chatTiddlersInWorkspace = chatsByWorkspace[workspaceIndex];
|
||||
return chatTiddlersInWorkspace.map((tiddler) => {
|
||||
const chatItem: IChatListItem = {
|
||||
id: `${workspace.id}:${tiddler.title}`,
|
||||
id: tiddler.title,
|
||||
title: (tiddler.caption as string | undefined) ?? tiddler.title,
|
||||
chatJSONString: tiddler.text,
|
||||
chatJSON: JSON.parse(tiddler.text) as SingleChatState,
|
||||
|
|
@ -91,14 +97,15 @@ export function useChatsFromWiki(workspacesList: IWorkspaceWithMetadata[] | unde
|
|||
return chatItems;
|
||||
}
|
||||
|
||||
export async function addChatToWiki(newItem: IChatListItem, oldItem?: IChatListItem) {
|
||||
export async function addChatToWiki(newItem: IChatListItem) {
|
||||
await window.service.wiki.wikiOperation(
|
||||
WikiChannel.addTiddler,
|
||||
newItem.workspaceID,
|
||||
newItem.title,
|
||||
newItem.id,
|
||||
// eslint-disable-next-line @typescript-eslint/strict-boolean-expressions, @typescript-eslint/prefer-nullish-coalescing
|
||||
newItem.chatJSONString || '[]',
|
||||
{
|
||||
caption: newItem.title,
|
||||
type: 'application/json',
|
||||
tags: [...newItem.tags, chatTiddlerTagName],
|
||||
description: newItem.description ?? '',
|
||||
|
|
@ -107,13 +114,6 @@ export async function addChatToWiki(newItem: IChatListItem, oldItem?: IChatListI
|
|||
},
|
||||
{ withDate: true },
|
||||
);
|
||||
if (oldItem !== undefined) {
|
||||
window.service.wiki.wikiOperation(
|
||||
WikiChannel.deleteTiddler,
|
||||
oldItem.workspaceID,
|
||||
oldItem.title,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export function sortChat(a: IChatListItem, b: IChatListItem) {
|
||||
|
|
@ -127,33 +127,20 @@ export function sortChat(a: IChatListItem, b: IChatListItem) {
|
|||
* @param workflowID The workflow ID that these chats were generated with.
|
||||
*/
|
||||
export function useChatDataSource(workspacesList: IWorkspaceWithMetadata[] | undefined, workflowID: string | undefined) {
|
||||
const [chats, setChats] = useState<IChatListItem[]>([]);
|
||||
const initialChats = useChatsFromWiki(workspacesList, workflowID);
|
||||
const workspaceID = useWorkspaceIDToStoreNewChats(workspacesList);
|
||||
const {
|
||||
updateChats,
|
||||
addChat,
|
||||
removeChat,
|
||||
chatList,
|
||||
} = useChatsStore((state) => ({
|
||||
updateChats: state.updateChats,
|
||||
addChat: state.addChat,
|
||||
removeChat: state.removeChat,
|
||||
chatList: Object.values(state.chats).filter((item): item is IChatListItem => item !== undefined).sort((a, b) => sortChat(a, b)),
|
||||
}));
|
||||
|
||||
useEffect(() => {
|
||||
setChats(initialChats.sort(sortChat));
|
||||
}, [initialChats]);
|
||||
|
||||
const onAddChat = useCallback(async (newItem: IChatListItem, oldItem?: IChatListItem) => {
|
||||
await addChatToWiki(newItem, oldItem);
|
||||
setChats((chats) => [...chats.filter(item => item.title !== newItem.title), newItem].sort(sortChat));
|
||||
}, []);
|
||||
|
||||
const onDeleteChat = useCallback((item: IChatListItem) => {
|
||||
window.service.wiki.wikiOperation(
|
||||
WikiChannel.deleteTiddler,
|
||||
item.workspaceID,
|
||||
item.title,
|
||||
);
|
||||
setChats((chats) => chats.filter(chat => chat.id !== item.id).sort(sortChat));
|
||||
}, []);
|
||||
|
||||
return [chats, onAddChat, onDeleteChat] as const;
|
||||
}
|
||||
|
||||
export function useLoadInitialChatDataToStore(workspacesList: IWorkspaceWithMetadata[] | undefined, workflowID: string | undefined) {
|
||||
const updateChats = useChatsStore((state) => state.updateChats);
|
||||
const [initialChats] = useChatDataSource(workspacesList, workflowID);
|
||||
useEffect(() => {
|
||||
const chatsDict = initialChats.reduce<Record<string, IChatListItem>>((accumulator, chat) => {
|
||||
accumulator[chat.id] = chat;
|
||||
|
|
@ -161,4 +148,42 @@ export function useLoadInitialChatDataToStore(workspacesList: IWorkspaceWithMeta
|
|||
}, {});
|
||||
updateChats(chatsDict);
|
||||
}, [initialChats, updateChats]);
|
||||
|
||||
const onAddChat = useCallback(async (newItemFields?: {
|
||||
title?: string | undefined;
|
||||
}) => {
|
||||
if (workspaceID === undefined || workflowID === undefined) return;
|
||||
const newItem = addChat({
|
||||
workflowID,
|
||||
workspaceID,
|
||||
...newItemFields,
|
||||
});
|
||||
await addChatToWiki(newItem);
|
||||
}, [addChat, workflowID, workspaceID]);
|
||||
|
||||
const onDeleteChat = useCallback((chatID: string) => {
|
||||
if (workspaceID === undefined) return;
|
||||
window.service.wiki.wikiOperation(
|
||||
WikiChannel.deleteTiddler,
|
||||
workspaceID,
|
||||
chatID,
|
||||
);
|
||||
removeChat(chatID);
|
||||
}, [removeChat, workspaceID]);
|
||||
|
||||
return [chatList, onAddChat, onDeleteChat] as const;
|
||||
}
|
||||
|
||||
// connect store and dataSource
|
||||
|
||||
export function useWorkspaceIDToStoreNewChats(workspacesList: IWorkspaceWithMetadata[] | undefined) {
|
||||
const [workspaceIDToStoreNewChats, setWorkspaceIDToStoreNewChats] = useState<string | undefined>();
|
||||
// set workspaceIDToStoreNewChats on initial load && workspacesList has value, make it default to save to first workspace.
|
||||
useEffect(() => {
|
||||
if (workspaceIDToStoreNewChats === undefined && workspacesList?.[0] !== undefined) {
|
||||
const workspaceID = workspacesList[0].id;
|
||||
setWorkspaceIDToStoreNewChats(workspaceID);
|
||||
}
|
||||
}, [workspaceIDToStoreNewChats, workspacesList]);
|
||||
return workspaceIDToStoreNewChats;
|
||||
}
|
||||
|
|
@ -14,7 +14,7 @@ import styled from 'styled-components';
|
|||
import { useLocation } from 'wouter';
|
||||
|
||||
import { DeleteConfirmationDialog } from './DeleteConfirmationDialog';
|
||||
import { useHandleOpenInTheGraphEditor } from './useClickHandler';
|
||||
import { useHandleOpenInTheGraphEditor, useHandleOpenInTheRunWorkflow } from './useClickHandler';
|
||||
import type { IWorkflowTiddler } from './useWorkflowDataSource';
|
||||
|
||||
const WorkflowListContainer = styled(Box)`
|
||||
|
|
@ -81,10 +81,9 @@ export function WorkflowListItem(props: IWorkflowListItemProps) {
|
|||
window.service.wiki.wikiOperation(WikiChannel.openTiddler, item.workspaceID, item.title);
|
||||
}, [item, setLocation]);
|
||||
|
||||
const handleOpenInTheGraphEditorRaw = useHandleOpenInTheGraphEditor();
|
||||
const handleOpenInTheGraphEditor = useCallback(() => {
|
||||
handleOpenInTheGraphEditorRaw(item);
|
||||
}, [handleOpenInTheGraphEditorRaw, item]);
|
||||
const handleOpenInTheGraphEditor = useHandleOpenInTheGraphEditor(item);
|
||||
const handleOpenInTheRunWorkflow = useHandleOpenInTheRunWorkflow(item);
|
||||
|
||||
const menuID = `workflow-list-item-menu-${item.id}`;
|
||||
return (
|
||||
<WorkflowCard>
|
||||
|
|
@ -108,7 +107,7 @@ export function WorkflowListItem(props: IWorkflowListItemProps) {
|
|||
</CardActionArea>
|
||||
<ItemMenuCardActions>
|
||||
<Button onClick={handleOpenInTheGraphEditor}>{t('Open')}</Button>
|
||||
<Button onClick={handleOpenInTheGraphEditor}>{t('Open')}</Button>
|
||||
<Button onClick={handleOpenInTheRunWorkflow}>{t('Workflow.Use')}</Button>
|
||||
<Button aria-controls={menuID} aria-haspopup='true' onClick={handleOpenItemMenu}>
|
||||
{anchorElement === null ? <MenuIcon /> : <MenuOpenIcon />}
|
||||
</Button>
|
||||
|
|
|
|||
|
|
@ -1,16 +1,29 @@
|
|||
/* eslint-disable @typescript-eslint/strict-boolean-expressions */
|
||||
import { PageType } from '@services/pages/interface';
|
||||
import { WindowNames } from '@services/windows/WindowProperties';
|
||||
import { useCallback, useContext } from 'react';
|
||||
import { MouseEvent, useCallback, useContext } from 'react';
|
||||
import { useLocation } from 'wouter';
|
||||
import { WorkflowContext } from '../GraphEditor/hooks/useContext';
|
||||
import type { IWorkflowListItem } from './WorkflowList';
|
||||
|
||||
export function useHandleOpenInTheGraphEditor() {
|
||||
export function useHandleOpenInTheGraphEditor(item?: IWorkflowListItem) {
|
||||
const [, setLocation] = useLocation();
|
||||
const workflowContext = useContext(WorkflowContext);
|
||||
const handleOpenInTheGraphEditor = useCallback((item: IWorkflowListItem) => {
|
||||
setLocation(`/${WindowNames.main}/${PageType.workflow}/${item.id}/`);
|
||||
const handleOpenInTheGraphEditor = useCallback((item1?: IWorkflowListItem | MouseEvent<HTMLButtonElement>) => {
|
||||
const workflowID = item?.id ?? (item1 as IWorkflowListItem)?.id;
|
||||
if (!workflowID) return;
|
||||
setLocation(`/${WindowNames.main}/${PageType.workflow}/workflow/${workflowID}/`);
|
||||
workflowContext.setOpenedWorkflowItem(item);
|
||||
}, [setLocation, workflowContext]);
|
||||
}, [setLocation, workflowContext, item]);
|
||||
return handleOpenInTheGraphEditor;
|
||||
}
|
||||
|
||||
export function useHandleOpenInTheRunWorkflow(item: IWorkflowListItem) {
|
||||
const [, setLocation] = useLocation();
|
||||
const workflowContext = useContext(WorkflowContext);
|
||||
const handleOpenInTheGraphEditor = useCallback(() => {
|
||||
setLocation(`/${WindowNames.main}/${PageType.workflow}/run/${item.id}/`);
|
||||
workflowContext.setOpenedWorkflowItem(item);
|
||||
}, [setLocation, workflowContext, item]);
|
||||
return handleOpenInTheGraphEditor;
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue