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,