Merge pull request #350 from devchat-ai/feat/chatmark

Feat/chatmark
This commit is contained in:
boob.yang 2023-12-07 09:09:47 +08:00 committed by GitHub
commit 29c571c3c2
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
8 changed files with 404 additions and 49 deletions

43
package-lock.json generated
View File

@ -1,12 +1,12 @@
{
"name": "devchat",
"version": "0.1.33",
"version": "0.1.55",
"lockfileVersion": 2,
"requires": true,
"packages": {
"": {
"name": "devchat",
"version": "0.1.33",
"version": "0.1.55",
"dependencies": {
"@emotion/react": "^11.10.8",
"@mantine/core": "^6.0.10",
@ -40,7 +40,6 @@
"unified": "^11.0.3",
"unist-util-visit": "^5.0.0",
"uuid": "^9.0.0",
"xmlrpc": "^1.3.2",
"yaml": "^2.3.2"
},
"devDependencies": {
@ -90,8 +89,7 @@
"vscode-test": "^1.6.1",
"webpack": "^5.76.3",
"webpack-cli": "^5.0.1",
"webpack-dev-server": "^4.13.3",
"xmlrpc": "^1.3.2"
"webpack-dev-server": "^4.13.3"
},
"engines": {
"vscode": "^1.75.0"
@ -13004,12 +13002,6 @@
"integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==",
"dev": true
},
"node_modules/sax": {
"version": "1.2.4",
"resolved": "https://registry.npmjs.org/sax/-/sax-1.2.4.tgz",
"integrity": "sha512-NqVDv9TpANUjFm0N8uM5GxL36UgKi9/atZw+x7YFnQ8ckwFGKrl4xX4yWtrey3UJm5nP1kUbnYgLopqWNSRhWw==",
"dev": true
},
"node_modules/scheduler": {
"version": "0.23.0",
"resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.23.0.tgz",
@ -15007,29 +14999,6 @@
}
}
},
"node_modules/xmlbuilder": {
"version": "8.2.2",
"resolved": "https://registry.npmjs.org/xmlbuilder/-/xmlbuilder-8.2.2.tgz",
"integrity": "sha512-eKRAFz04jghooy8muekqzo8uCSVNeyRedbuJrp0fovbLIi7wlsYtdUn3vBAAPq2Y3/0xMz2WMEUQ8yhVVO9Stw==",
"dev": true,
"engines": {
"node": ">=4.0"
}
},
"node_modules/xmlrpc": {
"version": "1.3.2",
"resolved": "https://registry.npmjs.org/xmlrpc/-/xmlrpc-1.3.2.tgz",
"integrity": "sha512-jQf5gbrP6wvzN71fgkcPPkF4bF/Wyovd7Xdff8d6/ihxYmgETQYSuTc+Hl+tsh/jmgPLro/Aro48LMFlIyEKKQ==",
"dev": true,
"dependencies": {
"sax": "1.2.x",
"xmlbuilder": "8.2.x"
},
"engines": {
"node": ">=0.8",
"npm": ">=1.0.0"
}
},
"node_modules/xtend": {
"version": "4.0.2",
"resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.2.tgz",
@ -24529,12 +24498,6 @@
"integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==",
"dev": true
},
"sax": {
"version": "1.2.4",
"resolved": "https://registry.npmjs.org/sax/-/sax-1.2.4.tgz",
"integrity": "sha512-NqVDv9TpANUjFm0N8uM5GxL36UgKi9/atZw+x7YFnQ8ckwFGKrl4xX4yWtrey3UJm5nP1kUbnYgLopqWNSRhWw==",
"dev": true
},
"scheduler": {
"version": "0.23.0",
"resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.23.0.tgz",

View File

@ -1,13 +1,34 @@
import * as React from 'react';
import { createRoot } from 'react-dom/client';
import { MantineProvider } from '@mantine/core';
import { MantineProvider, MantineThemeOverride } from '@mantine/core';
import { Provider, rootStore } from '@/views/stores/RootStore';
import App from '@/views/App';
const container = document.getElementById('app')!;
const root = createRoot(container); // createRoot(container!) if you use TypeScript
const myTheme: MantineThemeOverride = {
fontFamily: 'var(--vscode-editor-font-family)',
colors: {
"merico":[
"#F9F5F4",
"#EADAD6",
"#E1C0B6",
"#DEA594",
"#E1886F",
"#ED6A45",
"#D75E3C",
"#BD573B",
"#9F5541",
"#865143",
],
},
primaryColor: 'merico',
};
root.render(
<MantineProvider withGlobalStyles withNormalizeCSS>
<MantineProvider withGlobalStyles withNormalizeCSS withCSSVariables
theme={myTheme}>
<Provider value={rootStore}>
<App />
</Provider>

View File

@ -100,9 +100,11 @@ export interface CommandResult {
export class CommandRun {
private childProcess: any;
private _input: string;
// init childProcess in construction function
constructor() {
this._input = "";
this.childProcess = null;
}
@ -129,7 +131,8 @@ export class CommandRun {
let stderr = '';
this.childProcess.stdout.on('data', (data: { toString: () => any; }) => {
const dataStr = data.toString();
const dataStr = this._input + data.toString();
this._input = "";
if (onData) {
onData(dataStr);
}
@ -186,6 +189,7 @@ export class CommandRun {
public write(input: string) {
if (this.childProcess) {
this._input += input;
this.childProcess.stdin.write(input);
}
}

View File

@ -14,7 +14,7 @@ export default function App() {
styles={{
main: {
padding:'40px 0 0 0',
fontFamily: 'var(--vscode-editor-font-familyy)',
fontFamily: 'var(--vscode-editor-font-family)',
fontSize: 'var(--vscode-editor-font-size)',
},
}}

View File

@ -0,0 +1,286 @@
import React, { useEffect, useState } from 'react';
import { Box, Button, Checkbox, Text, Radio, Textarea, createStyles } from '@mantine/core';
import { useListState, useSetState } from '@mantine/hooks';
import { useMst } from '@/views/stores/RootStore';
import yaml from 'js-yaml';
const useStyles = createStyles((theme) => ({
container:{
padding:0,
margin:0,
},
submit:{
marginTop:theme.spacing.xs,
marginRight:theme.spacing.xs,
marginBottom:theme.spacing.xs,
},
cancel:{
},
button:{
marginTop:theme.spacing.xs,
marginRight:theme.spacing.xs,
marginBottom:theme.spacing.xs,
},
checkbox:{
marginTop:theme.spacing.xs,
marginBottom:theme.spacing.xs,
},
label:{
color:'var(--vscode-editor-foreground)',
},
radio:{
marginTop:theme.spacing.xs,
marginBottom:theme.spacing.xs,
},
editor:{
backgroundColor: 'var(--vscode-input-background)',
borderColor: 'var(--vscode-input-border)',
color: 'var(--vscode-input-foreground)',
},
editorWrapper:{
marginTop:theme.spacing.xs,
marginBottom:theme.spacing.xs,
}
}));
interface Wdiget{
id:string,
value:string,
title?:string,
type:'editor'|'checkbox'|'radio'|'button'|'text'
}
const ChatMark = ({ children,value }) => {
const {classes} = useStyles();
const [widgets,widgetsHandlers] = useListState<Wdiget>();
const {chat} = useMst();
const [autoForm,setAutoForm] = useState(false); // if any widget is checkbox,radio or editor wdiget, the form is auto around them
const values = value?yaml.load(value):{};
const [disabled,setDisabled] = useState(!!value);
const handleSubmit = () => {
let formData = {};
widgets.forEach((widget)=>{
if(widget.type === 'text'
|| widget.type === 'button'
|| (widget.type === 'radio' && widget.value === 'unchecked')
|| (widget.type === 'checkbox' && widget.value === 'unchecked')){
// ignore
return;
}
formData[widget.id] = widget.value;
});
chat.userInput(formData);
};
const handleCancel = () => {
chat.userInput({
'form':'canceled'
});
};
const handleButtonClick = ({event,index}) => {
const widget = widgets[index];
widget['value'] = event.currentTarget.value;;
widgetsHandlers.setItem(index,widget);
chat.userInput({
[widget['id']]:'clicked'
});
};
const handleCheckboxChange = ({event,index})=>{
const widget = widgets[index];
widget['value'] = event.currentTarget.checked?'checked':'unchecked';
widgetsHandlers.setItem(index,widget);
};
const handleRadioChange = ({event,allValues})=>{
widgetsHandlers.apply((item, index) => {
if(allValues.includes(item.id)){
if(item.id === event){
item.value = 'checked';
}else{
item.value = 'unchecked';
}
}
return item;
});
};
const handleEditorChange = ({event,index})=>{
const widget = widgets[index];
widget['value'] = event.currentTarget.value;
widgetsHandlers.setItem(index,widget);
};
useEffect(()=>{
const lines = children.split('\n');
let detectEditorId = '';
let editorContentRecorder = '';
const textRegex = /^([^>].*)/; // Text widget
const buttonRegex = /^>\s*\((.*?)\)\s*(.*)/; // Button widget
const checkboxRegex = /^>\s*\[([x ]*)\]\((.*?)\)\s*(.*)/; // Checkbox widget
const radioRegex = /^>\s*-\s*\((.*?)\)\s*(.*)/; // Radio button widget
const editorRegex = /^>\s*\|\s*\((.*?)\)/; // Editor widget
const editorContentRegex = /^>\s*(.*)/; // Editor widget
lines.forEach((line, index) => {
let match;
if (match = line.match(textRegex)) {
widgetsHandlers.append({
id:`text${index}`,
type:'text',
value:line,
});
} else if (match = line.match(buttonRegex)) {
const [id, title] = match.slice(1);
widgetsHandlers.append({
id,
title,
type:'button',
value:title,
});
} else if (match = line.match(checkboxRegex)) {
const [status, id, title] = match.slice(1);
widgetsHandlers.append({
id,
title,
type:'checkbox',
value: disabled?'unchecked':status === 'x'?'checked':'unchecked',
});
setAutoForm(true);
} else if (match = line.match(radioRegex)) {
const [id, title] = match.slice(1);
widgetsHandlers.append({
id,
title,
type:'radio',
value:'unchecked',
});
setAutoForm(true);
} else if (match = line.match(editorRegex)) {
const [id] = match.slice(1);
detectEditorId = id;
widgetsHandlers.append({
id,
type:'editor',
value: '',
});
setAutoForm(true);
} else if(match = line.match(editorContentRegex)){
const [content] = match.slice(1);
editorContentRecorder += content + '\n';
}
// if next line is not editor, then end current editor
const nextLine = index + 1 < lines.length? lines[index + 1]:null;
if (detectEditorId && (!nextLine || !nextLine.startsWith('>'))) {
// remove last \n
editorContentRecorder = editorContentRecorder.substring(0, editorContentRecorder.length - 1);
// apply editor content to widget
((editorId,editorContent) => widgetsHandlers.apply((item)=>{
if(item.id === editorId && !disabled){
item.value = editorContent;
}
return item;
}))(detectEditorId,editorContentRecorder);
// reset editor
detectEditorId = '';
editorContentRecorder = '';
}
});
for (const key in values) {
widgetsHandlers.apply((item)=>{
if(item.id === key){
item.value = values[key];
}
return item;
});
}
},[]);
// Render markdown widgets
const renderWidgets = (widgets) => {
let radioGroupTemp:any = [];
let radioValuesTemp:any = [];
let wdigetsTemp:any = [];
widgets.map((widget, index) => {
if (widget.type === 'text') {
wdigetsTemp.push(<Text key={index}>{widget.value}</Text>);
} else if (widget.type === 'button') {
wdigetsTemp.push(<Button
className={classes.button}
disabled={disabled}
key={'widget'+index}
size='xs'
value={widget.value}
onClick={event => handleButtonClick({event,index})}>
{widget.title}
</Button>);
} else if (widget.type === 'checkbox') {
wdigetsTemp.push(<Checkbox
classNames={{root:classes.checkbox,label:classes.label}}
disabled={disabled}
key={'widget'+index}
label={widget.title}
checked={widget.value==='checked'}
size='xs'
onChange={event => handleCheckboxChange({event,index})}/>);
} else if (widget.type === 'radio') {
radioValuesTemp.push(widget.id);
radioGroupTemp.push(<Radio
classNames={{root:classes.radio,label:classes.label}}
disabled={disabled}
key={'widget'+index}
label={widget.title}
value={widget.id}
size='xs' />);
// if next widget is not radio, then end current group
const nextWidget = index + 1 < widgets.length? widgets[index + 1]:null;
if (!nextWidget || nextWidget.type !== 'radio') {
const radioGroup = ((radios,allValues)=><Radio.Group
key={`radio-group-${index}`}
onChange={
event => handleRadioChange({
event,
allValues
})
}>
{radios}
</Radio.Group>)(radioGroupTemp,radioValuesTemp);
radioGroupTemp = [];
radioValuesTemp = [];
wdigetsTemp.push(radioGroup);
}
} else if (widget.type === 'editor') {
wdigetsTemp.push(<Textarea
disabled={disabled}
autosize
classNames={{wrapper:classes.editorWrapper,input:classes.editor}}
key={'widget'+index}
defaultValue={widget.value}
maxRows={10}
onChange={event => handleEditorChange({event,index})}/>);
}
});
return wdigetsTemp;
};
return (
<Box className={classes.container}>
{autoForm && !disabled
?<form>
{renderWidgets(widgets)}
<Box>
<Button className={classes.submit} size='xs' onClick={handleSubmit}>Submit</Button>
<Button className={classes.cancel} size='xs' onClick={handleCancel}>Cancel</Button>
</Box>
</form>
:renderWidgets(widgets)
}
</Box>
);
};
export default ChatMark;

View File

@ -14,7 +14,7 @@ import { IChatContext } from "@/views/stores/InputStore";
interface IProps {
item?: IMessage,
avatarType?: "user" | "bot" | "system",
avatarType?: string,
copyMessage?: string,
messageContexts?: IChatContext[],
deleteHash?: string,

View File

@ -13,6 +13,9 @@ import { Message } from "@/views/stores/ChatStore";
import messageUtil from '@/util/MessageUtil';
import {fromMarkdown} from 'mdast-util-from-markdown';
import {visit} from 'unist-util-visit';
import ChatMark from "@/views/components/ChatMark";
import { useSetState } from "@mantine/hooks";
import { toMarkdown } from "mdast-util-to-markdown";
interface MessageMarkdownProps extends React.ComponentProps<typeof ReactMarkdown> {
children: string,
@ -26,6 +29,17 @@ type Step = {
endsWithTripleBacktick: boolean;
};
function parseMetaData(string) {
const regexp = /((?<k1>(?!=)\S+)=((?<v1>(["'`])(.*?)\5)|(?<v2>\S+)))|(?<k2>\S+)/g;
const io = (string ?? '').matchAll(regexp);
return new Map(
[...io]
.map((item) => item?.groups)
.map(({ k1, k2, v1, v2 }) => [k1 ?? k2, v1 ?? v2]),
);
}
const MessageMarkdown = observer((props: MessageMarkdownProps) => {
const { children,temp=false } = props;
const { chat } = useMst();
@ -33,7 +47,7 @@ const MessageMarkdown = observer((props: MessageMarkdownProps) => {
const tree = fromMarkdown(children);
const codes = tree.children.filter(node => node.type === 'code');
const lastNode = tree.children[tree.children.length-1];
let index = 1;
const [chatmarkValues,setChatmarkValues] = useSetState({});
const handleExplain = (value: string | undefined) => {
console.log(value);
@ -119,19 +133,65 @@ Generate a professionally written and formatted release note in markdown with th
}
};
useEffect(()=>{
let previousNode:any = null;
let chatmarkCount = 0;
visit(tree, function (node) {
if (node.type === 'code') {
// set meta data as props
const metaData = parseMetaData(node.meta);
let props = {...metaData};
if(node.lang ==='chatmark' || node.lang ==='ChatMark'){
props['index'] = chatmarkCount;
} else if ((node.lang === 'yaml' || node.lang === 'YAML') && previousNode && previousNode.type === 'code' && previousNode.lang === 'chatmark') {
setChatmarkValues({[`chatmark-${previousNode.data.hProperties.index}`]:node.value});
}
node.data={
hProperties:{
...props
}
};
// record node and count data for next loop
previousNode = node;
if(node.lang ==='chatmark' || node.lang ==='ChatMark'){
chatmarkCount++;
}
}
});
},[children]);
return <ReactMarkdown
{...props}
remarkPlugins={[()=> (tree) =>{
let stepCount = 1;
let chatmarkCount = 0;
visit(tree, function (node) {
if (node.type === 'code' && (node.lang ==='step' || node.lang ==='Step')) {
node.data = {
if (node.type === 'code') {
// set meta data as props
const metaData = parseMetaData(node.meta);
let props = {...metaData};
if(node.lang ==='step' || node.lang ==='Step'){
props['index'] = stepCount;
} else if(node.lang ==='chatmark' || node.lang ==='ChatMark'){
props['id'] = `chatmark-${chatmarkCount}`;
props['index'] = chatmarkCount;
}
node.data={
hProperties:{
index: index++
...props
}
};
// record node and count data for next loop
if(node.lang ==='chatmark' || node.lang ==='ChatMark'){
chatmarkCount++;
}
if(node.lang ==='step' || node.lang ==='Step'){
stepCount++;
}
}
});
}]}
rehypePlugins={[rehypeRaw]}
components={{
code({ node, inline, className, children, index, ...props }) {
@ -153,6 +213,11 @@ Generate a professionally written and formatted release note in markdown with th
return <Step language={lanugage} done={temp?done:true}>{value}</Step>;
}
if (lanugage === 'chatmark' || lanugage === 'ChatMark') {
const chatmarkValue = chatmarkValues[`chatmark-${index}`];
return <ChatMark value={chatmarkValue}>{value}</ChatMark>;
}
return !inline && lanugage ? (
<div style={{ position: 'relative' }}>
<LanguageCorner language={lanugage} />

View File

@ -3,6 +3,7 @@ import messageUtil from '@/util/MessageUtil';
import { ChatContext } from '@/views/stores/InputStore';
import { features } from "process";
import { Slice } from "@tiptap/pm/model";
import yaml from 'js-yaml';
interface Context {
content: string;
@ -201,6 +202,20 @@ You can configure DevChat from [Settings](#settings).`;
goScrollBottom();
};
const userInput = (values:any) => {
const inputStr = `
\`\`\`yaml type=chatmark-values
${yaml.dump(values)}
\`\`\`
`;
self.currentMessage = self.currentMessage + inputStr;
messageUtil.sendMessage({
command: 'userInput',
text: inputStr
});
// goto bottom
goScrollBottom();
};
return {
helpMessage,
@ -209,6 +224,7 @@ You can configure DevChat from [Settings](#settings).`;
goScrollBottom,
startGenerating,
commonMessage,
userInput,
devchatAsk : flow(function* (userMessage, chatContexts) {
self.messages.push({
type: 'user',