feat: create new chat in the store and wiki

This commit is contained in:
linonetwo 2023-08-31 17:41:06 +08:00
parent 45d12ce1a0
commit 083eda4e6d
9 changed files with 277 additions and 52 deletions

View file

@ -488,7 +488,9 @@
"InPort": "In Ports", "InPort": "In Ports",
"StopWorkflow": "Stop Workflow", "StopWorkflow": "Stop Workflow",
"ToggleDebugPanel": "Toggle Debug Panel", "ToggleDebugPanel": "Toggle Debug Panel",
"ClearDebugPanel": "Clear Debug Panel" "ClearDebugPanel": "Clear Debug Panel",
"NewChat": "New Chat",
"DeleteChat": "Delete Chat"
}, },
"Description": "Description", "Description": "Description",
"Tags": "Tags", "Tags": "Tags",

View file

@ -493,6 +493,8 @@
"InPort": "入口", "InPort": "入口",
"StopWorkflow": "停止工作流", "StopWorkflow": "停止工作流",
"ToggleDebugPanel": "切换开关调试面板", "ToggleDebugPanel": "切换开关调试面板",
"ClearDebugPanel": "清空调试面板" "ClearDebugPanel": "清空调试面板",
"NewChat": "新对话",
"DeleteChat": "删除对话"
} }
} }

View 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>
);
};

View 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>
);
};

View 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>
);
};

View file

@ -8,9 +8,10 @@ export interface ChatsStoreState {
chats: Record<string, IChatListItem | undefined>; chats: Record<string, IChatListItem | undefined>;
} }
export interface ChatsStoreActions { 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; addElementToChat: (chatID: string, element: Pick<UIElementState, 'type' | 'props' | 'author'>) => string;
clearElementsInChat: (chatID: string) => void; clearElementsInChat: (chatID: string) => void;
getChat: (chatID: string) => IChatListItem | undefined;
removeChat: (chatID: string) => void; removeChat: (chatID: string) => void;
removeElementFromChat: (chatID: string, id: string) => void; removeElementFromChat: (chatID: string, id: string) => void;
submitElementInChat: (chatID: string, id: string, content: unknown) => 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) => { addChat: (fields) => {
const id = String(Math.random()); const id = String(Math.random());
const newChatItem = { chatJSON: { elements: {} }, id, tags: [], title: 'New Chat', ...fields };
set((state) => { 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 }) => { addElementToChat: (chatID, { type, props, author }) => {

View file

@ -28,6 +28,9 @@ export interface IChatListItem {
*/ */
chatJSONString?: string; chatJSONString?: string;
description?: string; description?: string;
/**
* Random generated ID
*/
id: string; id: string;
image?: string; image?: string;
metadata?: { metadata?: {
@ -35,6 +38,9 @@ export interface IChatListItem {
workspace: IWorkspaceWithMetadata; workspace: IWorkspaceWithMetadata;
}; };
tags: string[]; tags: string[];
/**
* From caption field, or use ID
*/
title: string; title: string;
workflowID: string; workflowID: string;
workspaceID: string; workspaceID: string;
@ -67,7 +73,7 @@ export function useChatsFromWiki(workspacesList: IWorkspaceWithMetadata[] | unde
const chatTiddlersInWorkspace = chatsByWorkspace[workspaceIndex]; const chatTiddlersInWorkspace = chatsByWorkspace[workspaceIndex];
return chatTiddlersInWorkspace.map((tiddler) => { return chatTiddlersInWorkspace.map((tiddler) => {
const chatItem: IChatListItem = { const chatItem: IChatListItem = {
id: `${workspace.id}:${tiddler.title}`, id: tiddler.title,
title: (tiddler.caption as string | undefined) ?? tiddler.title, title: (tiddler.caption as string | undefined) ?? tiddler.title,
chatJSONString: tiddler.text, chatJSONString: tiddler.text,
chatJSON: JSON.parse(tiddler.text) as SingleChatState, chatJSON: JSON.parse(tiddler.text) as SingleChatState,
@ -91,14 +97,15 @@ export function useChatsFromWiki(workspacesList: IWorkspaceWithMetadata[] | unde
return chatItems; return chatItems;
} }
export async function addChatToWiki(newItem: IChatListItem, oldItem?: IChatListItem) { export async function addChatToWiki(newItem: IChatListItem) {
await window.service.wiki.wikiOperation( await window.service.wiki.wikiOperation(
WikiChannel.addTiddler, WikiChannel.addTiddler,
newItem.workspaceID, newItem.workspaceID,
newItem.title, newItem.id,
// eslint-disable-next-line @typescript-eslint/strict-boolean-expressions, @typescript-eslint/prefer-nullish-coalescing // eslint-disable-next-line @typescript-eslint/strict-boolean-expressions, @typescript-eslint/prefer-nullish-coalescing
newItem.chatJSONString || '[]', newItem.chatJSONString || '[]',
{ {
caption: newItem.title,
type: 'application/json', type: 'application/json',
tags: [...newItem.tags, chatTiddlerTagName], tags: [...newItem.tags, chatTiddlerTagName],
description: newItem.description ?? '', description: newItem.description ?? '',
@ -107,13 +114,6 @@ export async function addChatToWiki(newItem: IChatListItem, oldItem?: IChatListI
}, },
{ withDate: true }, { withDate: true },
); );
if (oldItem !== undefined) {
window.service.wiki.wikiOperation(
WikiChannel.deleteTiddler,
oldItem.workspaceID,
oldItem.title,
);
}
} }
export function sortChat(a: IChatListItem, b: IChatListItem) { 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. * @param workflowID The workflow ID that these chats were generated with.
*/ */
export function useChatDataSource(workspacesList: IWorkspaceWithMetadata[] | undefined, workflowID: string | undefined) { export function useChatDataSource(workspacesList: IWorkspaceWithMetadata[] | undefined, workflowID: string | undefined) {
const [chats, setChats] = useState<IChatListItem[]>([]);
const initialChats = useChatsFromWiki(workspacesList, workflowID); 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(() => { useEffect(() => {
const chatsDict = initialChats.reduce<Record<string, IChatListItem>>((accumulator, chat) => { const chatsDict = initialChats.reduce<Record<string, IChatListItem>>((accumulator, chat) => {
accumulator[chat.id] = chat; accumulator[chat.id] = chat;
@ -161,4 +148,42 @@ export function useLoadInitialChatDataToStore(workspacesList: IWorkspaceWithMeta
}, {}); }, {});
updateChats(chatsDict); updateChats(chatsDict);
}, [initialChats, updateChats]); }, [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;
}

View file

@ -14,7 +14,7 @@ import styled from 'styled-components';
import { useLocation } from 'wouter'; import { useLocation } from 'wouter';
import { DeleteConfirmationDialog } from './DeleteConfirmationDialog'; import { DeleteConfirmationDialog } from './DeleteConfirmationDialog';
import { useHandleOpenInTheGraphEditor } from './useClickHandler'; import { useHandleOpenInTheGraphEditor, useHandleOpenInTheRunWorkflow } from './useClickHandler';
import type { IWorkflowTiddler } from './useWorkflowDataSource'; import type { IWorkflowTiddler } from './useWorkflowDataSource';
const WorkflowListContainer = styled(Box)` const WorkflowListContainer = styled(Box)`
@ -81,10 +81,9 @@ export function WorkflowListItem(props: IWorkflowListItemProps) {
window.service.wiki.wikiOperation(WikiChannel.openTiddler, item.workspaceID, item.title); window.service.wiki.wikiOperation(WikiChannel.openTiddler, item.workspaceID, item.title);
}, [item, setLocation]); }, [item, setLocation]);
const handleOpenInTheGraphEditorRaw = useHandleOpenInTheGraphEditor(); const handleOpenInTheGraphEditor = useHandleOpenInTheGraphEditor(item);
const handleOpenInTheGraphEditor = useCallback(() => { const handleOpenInTheRunWorkflow = useHandleOpenInTheRunWorkflow(item);
handleOpenInTheGraphEditorRaw(item);
}, [handleOpenInTheGraphEditorRaw, item]);
const menuID = `workflow-list-item-menu-${item.id}`; const menuID = `workflow-list-item-menu-${item.id}`;
return ( return (
<WorkflowCard> <WorkflowCard>
@ -108,7 +107,7 @@ export function WorkflowListItem(props: IWorkflowListItemProps) {
</CardActionArea> </CardActionArea>
<ItemMenuCardActions> <ItemMenuCardActions>
<Button onClick={handleOpenInTheGraphEditor}>{t('Open')}</Button> <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}> <Button aria-controls={menuID} aria-haspopup='true' onClick={handleOpenItemMenu}>
{anchorElement === null ? <MenuIcon /> : <MenuOpenIcon />} {anchorElement === null ? <MenuIcon /> : <MenuOpenIcon />}
</Button> </Button>

View file

@ -1,16 +1,29 @@
/* eslint-disable @typescript-eslint/strict-boolean-expressions */
import { PageType } from '@services/pages/interface'; import { PageType } from '@services/pages/interface';
import { WindowNames } from '@services/windows/WindowProperties'; import { WindowNames } from '@services/windows/WindowProperties';
import { useCallback, useContext } from 'react'; import { MouseEvent, useCallback, useContext } from 'react';
import { useLocation } from 'wouter'; import { useLocation } from 'wouter';
import { WorkflowContext } from '../GraphEditor/hooks/useContext'; import { WorkflowContext } from '../GraphEditor/hooks/useContext';
import type { IWorkflowListItem } from './WorkflowList'; import type { IWorkflowListItem } from './WorkflowList';
export function useHandleOpenInTheGraphEditor() { export function useHandleOpenInTheGraphEditor(item?: IWorkflowListItem) {
const [, setLocation] = useLocation(); const [, setLocation] = useLocation();
const workflowContext = useContext(WorkflowContext); const workflowContext = useContext(WorkflowContext);
const handleOpenInTheGraphEditor = useCallback((item: IWorkflowListItem) => { const handleOpenInTheGraphEditor = useCallback((item1?: IWorkflowListItem | MouseEvent<HTMLButtonElement>) => {
setLocation(`/${WindowNames.main}/${PageType.workflow}/${item.id}/`); const workflowID = item?.id ?? (item1 as IWorkflowListItem)?.id;
if (!workflowID) return;
setLocation(`/${WindowNames.main}/${PageType.workflow}/workflow/${workflowID}/`);
workflowContext.setOpenedWorkflowItem(item); 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; return handleOpenInTheGraphEditor;
} }