diff --git a/src/components/graph/widgets/ChatHistoryWidget.vue b/src/components/graph/widgets/ChatHistoryWidget.vue new file mode 100644 index 0000000000..010f91ce76 --- /dev/null +++ b/src/components/graph/widgets/ChatHistoryWidget.vue @@ -0,0 +1,135 @@ + + + diff --git a/src/components/graph/widgets/DomWidget.vue b/src/components/graph/widgets/DomWidget.vue index 75e764ccc5..f8309f4a5d 100644 --- a/src/components/graph/widgets/DomWidget.vue +++ b/src/components/graph/widgets/DomWidget.vue @@ -11,6 +11,7 @@ v-if="isComponentWidget(widget)" :model-value="widget.value" :widget="widget" + v-bind="widget.props" @update:model-value="emit('update:widgetValue', $event)" /> diff --git a/src/components/graph/widgets/chatHistory/CopyButton.vue b/src/components/graph/widgets/chatHistory/CopyButton.vue new file mode 100644 index 0000000000..9f857ed649 --- /dev/null +++ b/src/components/graph/widgets/chatHistory/CopyButton.vue @@ -0,0 +1,36 @@ + + + diff --git a/src/components/graph/widgets/chatHistory/ResponseBlurb.vue b/src/components/graph/widgets/chatHistory/ResponseBlurb.vue new file mode 100644 index 0000000000..0d0997f317 --- /dev/null +++ b/src/components/graph/widgets/chatHistory/ResponseBlurb.vue @@ -0,0 +1,22 @@ + + + diff --git a/src/composables/node/useNodeChatHistory.ts b/src/composables/node/useNodeChatHistory.ts new file mode 100644 index 0000000000..72af8666c4 --- /dev/null +++ b/src/composables/node/useNodeChatHistory.ts @@ -0,0 +1,60 @@ +import { LGraphNode } from '@comfyorg/litegraph' + +import type ChatHistoryWidget from '@/components/graph/widgets/ChatHistoryWidget.vue' +import { useChatHistoryWidget } from '@/composables/widgets/useChatHistoryWidget' + +const CHAT_HISTORY_WIDGET_NAME = '$$node-chat-history' + +/** + * Composable for handling node text previews + */ +export function useNodeChatHistory( + options: { + minHeight?: number + props?: Omit['$props'], 'widget'> + } = {} +) { + const chatHistoryWidget = useChatHistoryWidget(options) + + const findChatHistoryWidget = (node: LGraphNode) => + node.widgets?.find((w) => w.name === CHAT_HISTORY_WIDGET_NAME) + + const addChatHistoryWidget = (node: LGraphNode) => + chatHistoryWidget(node, { + name: CHAT_HISTORY_WIDGET_NAME, + type: 'chatHistory' + }) + + /** + * Shows chat history for a node + * @param node The graph node to show the chat history for + */ + function showChatHistory(node: LGraphNode) { + if (!findChatHistoryWidget(node)) { + addChatHistoryWidget(node) + } + node.setDirtyCanvas?.(true) + } + + /** + * Removes chat history from a node + * @param node The graph node to remove the chat history from + */ + function removeChatHistory(node: LGraphNode) { + if (!node.widgets) return + + const widgetIdx = node.widgets.findIndex( + (w) => w.name === CHAT_HISTORY_WIDGET_NAME + ) + + if (widgetIdx > -1) { + node.widgets[widgetIdx].onRemove?.() + node.widgets.splice(widgetIdx, 1) + } + } + + return { + showChatHistory, + removeChatHistory + } +} diff --git a/src/composables/widgets/useChatHistoryWidget.ts b/src/composables/widgets/useChatHistoryWidget.ts new file mode 100644 index 0000000000..9f807a3c6c --- /dev/null +++ b/src/composables/widgets/useChatHistoryWidget.ts @@ -0,0 +1,43 @@ +import type { LGraphNode } from '@comfyorg/litegraph' +import { ref } from 'vue' + +import ChatHistoryWidget from '@/components/graph/widgets/ChatHistoryWidget.vue' +import type { InputSpec } from '@/schemas/nodeDef/nodeDefSchemaV2' +import { ComponentWidgetImpl, addWidget } from '@/scripts/domWidget' +import type { ComfyWidgetConstructorV2 } from '@/scripts/widgets' + +const PADDING = 16 + +export const useChatHistoryWidget = ( + options: { + props?: Omit['$props'], 'widget'> + } = {} +) => { + const widgetConstructor: ComfyWidgetConstructorV2 = ( + node: LGraphNode, + inputSpec: InputSpec + ) => { + const widgetValue = ref('') + const widget = new ComponentWidgetImpl< + string | object, + InstanceType['$props'] + >({ + node, + name: inputSpec.name, + component: ChatHistoryWidget, + props: options.props, + inputSpec, + options: { + getValue: () => widgetValue.value, + setValue: (value: string | object) => { + widgetValue.value = typeof value === 'string' ? value : String(value) + }, + getMinHeight: () => 400 + PADDING + } + }) + addWidget(node, widget) + return widget + } + + return widgetConstructor +} diff --git a/src/composables/widgets/useProgressTextWidget.ts b/src/composables/widgets/useProgressTextWidget.ts index f6c56efbda..7ab520e513 100644 --- a/src/composables/widgets/useProgressTextWidget.ts +++ b/src/composables/widgets/useProgressTextWidget.ts @@ -8,7 +8,11 @@ import type { ComfyWidgetConstructorV2 } from '@/scripts/widgets' const PADDING = 16 -export const useTextPreviewWidget = () => { +export const useTextPreviewWidget = ( + options: { + minHeight?: number + } = {} +) => { const widgetConstructor: ComfyWidgetConstructorV2 = ( node: LGraphNode, inputSpec: InputSpec @@ -24,7 +28,7 @@ export const useTextPreviewWidget = () => { setValue: (value: string | object) => { widgetValue.value = typeof value === 'string' ? value : String(value) }, - getMinHeight: () => 42 + PADDING + getMinHeight: () => options.minHeight ?? 42 + PADDING } }) addWidget(node, widget) diff --git a/src/locales/en/main.json b/src/locales/en/main.json index 3d32327609..f606be2d03 100644 --- a/src/locales/en/main.json +++ b/src/locales/en/main.json @@ -114,7 +114,9 @@ "learnMore": "Learn more", "amount": "Amount", "unknownError": "Unknown error", - "title": "Title" + "title": "Title", + "edit": "Edit", + "copy": "Copy" }, "manager": { "title": "Custom Nodes Manager", @@ -1406,5 +1408,12 @@ "tooltip": "Execute to selected output nodes (Highlighted with orange border)", "disabledTooltip": "No output nodes selected" } + }, + "chatHistory": { + "cancelEdit": "Cancel", + "editTooltip": "Edit message", + "cancelEditTooltip": "Cancel edit", + "copiedTooltip": "Copied", + "copyTooltip": "Copy message to clipboard" } } \ No newline at end of file diff --git a/src/locales/es/main.json b/src/locales/es/main.json index 84c0f39939..cd796f3b55 100644 --- a/src/locales/es/main.json +++ b/src/locales/es/main.json @@ -102,6 +102,13 @@ "title": "Crea una cuenta" } }, + "chatHistory": { + "cancelEdit": "Cancelar", + "cancelEditTooltip": "Cancelar edición", + "copiedTooltip": "Copiado", + "copyTooltip": "Copiar mensaje al portapapeles", + "editTooltip": "Editar mensaje" + }, "clipboard": { "errorMessage": "Error al copiar al portapapeles", "errorNotSupported": "API del portapapeles no soportada en su navegador", @@ -277,6 +284,7 @@ "continue": "Continuar", "control_after_generate": "control después de generar", "control_before_generate": "control antes de generar", + "copy": "Copiar", "copyToClipboard": "Copiar al portapapeles", "currentUser": "Usuario actual", "customize": "Personalizar", @@ -288,6 +296,7 @@ "disableAll": "Deshabilitar todo", "disabling": "Deshabilitando", "download": "Descargar", + "edit": "Editar", "empty": "Vacío", "enableAll": "Habilitar todo", "enabled": "Habilitado", diff --git a/src/locales/fr/main.json b/src/locales/fr/main.json index 68d7c0c1e2..b70a37f7af 100644 --- a/src/locales/fr/main.json +++ b/src/locales/fr/main.json @@ -102,6 +102,13 @@ "title": "Créer un compte" } }, + "chatHistory": { + "cancelEdit": "Annuler", + "cancelEditTooltip": "Annuler la modification", + "copiedTooltip": "Copié", + "copyTooltip": "Copier le message dans le presse-papiers", + "editTooltip": "Modifier le message" + }, "clipboard": { "errorMessage": "Échec de la copie dans le presse-papiers", "errorNotSupported": "L'API du presse-papiers n'est pas prise en charge par votre navigateur", @@ -277,6 +284,7 @@ "continue": "Continuer", "control_after_generate": "contrôle après génération", "control_before_generate": "contrôle avant génération", + "copy": "Copier", "copyToClipboard": "Copier dans le presse-papiers", "currentUser": "Utilisateur actuel", "customize": "Personnaliser", @@ -288,6 +296,7 @@ "disableAll": "Désactiver tout", "disabling": "Désactivation", "download": "Télécharger", + "edit": "Modifier", "empty": "Vide", "enableAll": "Activer tout", "enabled": "Activé", diff --git a/src/locales/ja/main.json b/src/locales/ja/main.json index dd37f40030..0bbc422cef 100644 --- a/src/locales/ja/main.json +++ b/src/locales/ja/main.json @@ -102,6 +102,13 @@ "title": "アカウントを作成する" } }, + "chatHistory": { + "cancelEdit": "キャンセル", + "cancelEditTooltip": "編集をキャンセル", + "copiedTooltip": "コピーしました", + "copyTooltip": "メッセージをクリップボードにコピー", + "editTooltip": "メッセージを編集" + }, "clipboard": { "errorMessage": "クリップボードへのコピーに失敗しました", "errorNotSupported": "お使いのブラウザではクリップボードAPIがサポートされていません", @@ -277,6 +284,7 @@ "continue": "続ける", "control_after_generate": "生成後の制御", "control_before_generate": "生成前の制御", + "copy": "コピー", "copyToClipboard": "クリップボードにコピー", "currentUser": "現在のユーザー", "customize": "カスタマイズ", @@ -288,6 +296,7 @@ "disableAll": "すべて無効にする", "disabling": "無効化", "download": "ダウンロード", + "edit": "編集", "empty": "空", "enableAll": "すべて有効にする", "enabled": "有効", diff --git a/src/locales/ko/main.json b/src/locales/ko/main.json index 3fccd2b396..9698000d85 100644 --- a/src/locales/ko/main.json +++ b/src/locales/ko/main.json @@ -102,6 +102,13 @@ "title": "계정 생성" } }, + "chatHistory": { + "cancelEdit": "취소", + "cancelEditTooltip": "편집 취소", + "copiedTooltip": "복사됨", + "copyTooltip": "메시지를 클립보드에 복사", + "editTooltip": "메시지 편집" + }, "clipboard": { "errorMessage": "클립보드에 복사하지 못했습니다", "errorNotSupported": "브라우저가 클립보드 API를 지원하지 않습니다.", @@ -277,6 +284,7 @@ "continue": "계속", "control_after_generate": "생성 후 제어", "control_before_generate": "생성 전 제어", + "copy": "복사", "copyToClipboard": "클립보드에 복사", "currentUser": "현재 사용자", "customize": "사용자 정의", @@ -288,6 +296,7 @@ "disableAll": "모두 비활성화", "disabling": "비활성화 중", "download": "다운로드", + "edit": "편집", "empty": "비어 있음", "enableAll": "모두 활성화", "enabled": "활성화됨", diff --git a/src/locales/ru/main.json b/src/locales/ru/main.json index 77817c905b..0debe8b250 100644 --- a/src/locales/ru/main.json +++ b/src/locales/ru/main.json @@ -102,6 +102,13 @@ "title": "Создать аккаунт" } }, + "chatHistory": { + "cancelEdit": "Отмена", + "cancelEditTooltip": "Отменить редактирование", + "copiedTooltip": "Скопировано", + "copyTooltip": "Скопировать сообщение в буфер", + "editTooltip": "Редактировать сообщение" + }, "clipboard": { "errorMessage": "Не удалось скопировать в буфер обмена", "errorNotSupported": "API буфера обмена не поддерживается в вашем браузере", @@ -277,6 +284,7 @@ "continue": "Продолжить", "control_after_generate": "управление после генерации", "control_before_generate": "управление до генерации", + "copy": "Копировать", "copyToClipboard": "Скопировать в буфер обмена", "currentUser": "Текущий пользователь", "customize": "Настроить", @@ -288,6 +296,7 @@ "disableAll": "Отключить все", "disabling": "Отключение", "download": "Скачать", + "edit": "Редактировать", "empty": "Пусто", "enableAll": "Включить все", "enabled": "Включено", diff --git a/src/locales/zh/main.json b/src/locales/zh/main.json index 663477217a..6d2b01a5bf 100644 --- a/src/locales/zh/main.json +++ b/src/locales/zh/main.json @@ -102,6 +102,13 @@ "title": "创建一个账户" } }, + "chatHistory": { + "cancelEdit": "取消", + "cancelEditTooltip": "取消编辑", + "copiedTooltip": "已复制", + "copyTooltip": "复制消息到剪贴板", + "editTooltip": "编辑消息" + }, "clipboard": { "errorMessage": "复制到剪贴板失败", "errorNotSupported": "您的浏览器不支持剪贴板API", @@ -277,6 +284,7 @@ "continue": "继续", "control_after_generate": "生成后控制", "control_before_generate": "生成前控制", + "copy": "复制", "copyToClipboard": "复制到剪贴板", "currentUser": "当前用户", "customize": "自定义", @@ -288,6 +296,7 @@ "disableAll": "禁用全部", "disabling": "禁用中", "download": "下载", + "edit": "编辑", "empty": "空", "enableAll": "启用全部", "enabled": "已启用", diff --git a/src/schemas/apiSchema.ts b/src/schemas/apiSchema.ts index 88990b3112..4b5a81ea0b 100644 --- a/src/schemas/apiSchema.ts +++ b/src/schemas/apiSchema.ts @@ -87,6 +87,12 @@ const zProgressTextWsMessage = z.object({ text: z.string() }) +const zDisplayComponentWsMessage = z.object({ + node_id: zNodeId, + component: z.enum(['ChatHistoryWidget']), + props: z.record(z.string(), z.any()).optional() +}) + const zTerminalSize = z.object({ cols: z.number(), row: z.number() @@ -120,6 +126,9 @@ export type ExecutionInterruptedWsMessage = z.infer< export type ExecutionErrorWsMessage = z.infer export type LogsWsMessage = z.infer export type ProgressTextWsMessage = z.infer +export type DisplayComponentWsMessage = z.infer< + typeof zDisplayComponentWsMessage +> // End of ws messages const zPromptInputItem = z.object({ diff --git a/src/scripts/api.ts b/src/scripts/api.ts index a40e8936d0..85316a74a7 100644 --- a/src/scripts/api.ts +++ b/src/scripts/api.ts @@ -1,6 +1,7 @@ import axios from 'axios' import type { + DisplayComponentWsMessage, EmbeddingsResponse, ExecutedWsMessage, ExecutingWsMessage, @@ -103,6 +104,7 @@ interface BackendApiCalls { /** Binary preview/progress data */ b_preview: Blob progress_text: ProgressTextWsMessage + display_component: DisplayComponentWsMessage } /** Dictionary of all api calls */ diff --git a/src/scripts/domWidget.ts b/src/scripts/domWidget.ts index 965099b6f4..6acfcd95f9 100644 --- a/src/scripts/domWidget.ts +++ b/src/scripts/domWidget.ts @@ -47,10 +47,13 @@ export interface DOMWidget /** * A DOM widget that wraps a Vue component as a litegraph widget. */ -export interface ComponentWidget - extends BaseDOMWidget { +export interface ComponentWidget< + V extends object | string, + P = Record +> extends BaseDOMWidget { readonly component: Component readonly inputSpec: InputSpec + readonly props?: P } export interface DOMWidgetOptions @@ -217,18 +220,23 @@ export class DOMWidgetImpl } } -export class ComponentWidgetImpl +export class ComponentWidgetImpl< + V extends object | string, + P = Record + > extends BaseDOMWidgetImpl - implements ComponentWidget + implements ComponentWidget { readonly component: Component readonly inputSpec: InputSpec + readonly props?: P constructor(obj: { node: LGraphNode name: string component: Component inputSpec: InputSpec + props?: P options: DOMWidgetOptions }) { super({ @@ -237,6 +245,7 @@ export class ComponentWidgetImpl }) this.component = obj.component this.inputSpec = obj.inputSpec + this.props = obj.props } override computeLayoutSize() { diff --git a/src/stores/executionStore.ts b/src/stores/executionStore.ts index 9ec7e86ca2..ea3bd36f93 100644 --- a/src/stores/executionStore.ts +++ b/src/stores/executionStore.ts @@ -1,8 +1,11 @@ import { defineStore } from 'pinia' import { computed, ref } from 'vue' +import type ChatHistoryWidget from '@/components/graph/widgets/ChatHistoryWidget.vue' +import { useNodeChatHistory } from '@/composables/node/useNodeChatHistory' import { useNodeProgressText } from '@/composables/node/useNodeProgressText' import type { + DisplayComponentWsMessage, ExecutedWsMessage, ExecutionCachedWsMessage, ExecutionErrorWsMessage, @@ -107,6 +110,10 @@ export const useExecutionStore = defineStore('execution', () => { ) } api.addEventListener('progress_text', handleProgressText as EventListener) + api.addEventListener( + 'display_component', + handleDisplayComponent as EventListener + ) function unbindExecutionEvents() { api.removeEventListener( @@ -195,6 +202,21 @@ export const useExecutionStore = defineStore('execution', () => { useNodeProgressText().showTextPreview(node, text) } + function handleDisplayComponent(e: CustomEvent) { + const { node_id, component, props = {} } = e.detail + const node = app.graph.getNodeById(node_id) + if (!node) return + + if (component === 'ChatHistoryWidget') { + useNodeChatHistory({ + props: props as Omit< + InstanceType['$props'], + 'widget' + > + }).showChatHistory(node) + } + } + function storePrompt({ nodes, id,