From 3cb591ed92894ea1dc2d8df1ee3128f7f45dceb4 Mon Sep 17 00:00:00 2001 From: yumoqing Date: Wed, 5 Aug 2026 10:55:50 +0800 Subject: [PATCH] feat: detail.ui face/voiceprint media card popups (DynamicColumn, image/audio playback) + tag buttons on cards and file list rows (tag_form.dspy -> add_tag.dspy, refresh media panel) --- wwwroot/knowledge_bases_list/detail.ui | 81 ++++++++++--- wwwroot/knowledge_bases_list/file_list.dspy | 5 + wwwroot/knowledge_bases_list/media_cards.dspy | 110 ++++++++++++++++++ wwwroot/knowledge_bases_list/tag_form.dspy | 82 +++++++++++++ 4 files changed, 259 insertions(+), 19 deletions(-) create mode 100644 wwwroot/knowledge_bases_list/media_cards.dspy create mode 100644 wwwroot/knowledge_bases_list/tag_form.dspy diff --git a/wwwroot/knowledge_bases_list/detail.ui b/wwwroot/knowledge_bases_list/detail.ui index f878fe6..fdcac2d 100644 --- a/wwwroot/knowledge_bases_list/detail.ui +++ b/wwwroot/knowledge_bases_list/detail.ui @@ -62,6 +62,68 @@ "text": "", "css": "filler" } + }, + { + "widgettype": "Button", + "options": { + "label": "👤 人脸", + "cfontsize": 12, + "bgcolor": "#e3f2fd", + "color": "#1565c0", + "padding": "4px 12px", + "borderRadius": "14px", + "marginLeft": "8px" + }, + "binds": [ + { + "wid": "self", + "event": "click", + "actiontype": "urlwidget", + "target": "PopupWindow", + "popup_options": { + "title": "👤 人脸库", + "width": "60%", + "height": "80%", + "movable": true, + "resizable": true, + "modal": true + }, + "options": { + "url": "{{entire_url('./media_cards.dspy')}}?kb_id={{params_kw.kb_id}}&kind=face" + } + } + ] + }, + { + "widgettype": "Button", + "options": { + "label": "🎤 声纹", + "cfontsize": 12, + "bgcolor": "#f3e5f5", + "color": "#7b1fa2", + "padding": "4px 12px", + "borderRadius": "14px", + "marginLeft": "8px" + }, + "binds": [ + { + "wid": "self", + "event": "click", + "actiontype": "urlwidget", + "target": "PopupWindow", + "popup_options": { + "title": "🎤 声纹库", + "width": "60%", + "height": "80%", + "movable": true, + "resizable": true, + "modal": true + }, + "options": { + "url": "{{entire_url('./media_cards.dspy')}}?kb_id={{params_kw.kb_id}}&kind=voice" + } + } + ] } ] }, @@ -84,25 +146,6 @@ "flexShrink": "0" }, "subwidgets": [ - { - "widgettype": "HBox", - "options": { - "padding": "10px 12px", - "bgcolor": "#f0f0f0", - "borderBottom": "1px solid #e0e0e0" - }, - "subwidgets": [ - { - "widgettype": "Text", - "options": { - "text": "📁 目录结构", - "cfontsize": 13, - "fontWeight": "bold", - "color": "#666" - } - } - ] - }, { "widgettype": "Tree", "id": "dir_tree", diff --git a/wwwroot/knowledge_bases_list/file_list.dspy b/wwwroot/knowledge_bases_list/file_list.dspy index 8397308..36194db 100644 --- a/wwwroot/knowledge_bases_list/file_list.dspy +++ b/wwwroot/knowledge_bases_list/file_list.dspy @@ -97,6 +97,11 @@ else: {"widgettype": "Text", "options": {"text": "⬇", "cfontsize": 14, "css": "clickable", "color": "#50b86c", "title": "下载"}, "binds": [{"wid": "self", "event": "click", "actiontype": "script", "target": "self", "script": "var a=document.createElement('a');a.href=" + json.dumps(file_url) + ";a.download='';document.body.appendChild(a);a.click();document.body.removeChild(a)"}]}, + {"widgettype": "Text", "options": {"text": "🏷", "cfontsize": 14, "css": "clickable", "color": "#1565c0", "title": "添加标签"}, + "binds": [{"wid": "self", "event": "click", "actiontype": "urlwidget", "target": "PopupWindow", + "popup_options": {"title": "为「" + display_name + "」添加标签", "width": "32%", "height": "auto", + "movable": True, "resizable": True, "modal": True}, + "options": {"url": "./tag_form.dspy?doc_id=" + f["id"] + "&kb_id=" + kb_id}}]}, {"widgettype": "Text", "options": {"text": "🗑", "cfontsize": 14, "css": "clickable", "color": "#f56c6c", "title": "删除"}, "binds": [{"wid": "self", "event": "click", "actiontype": "script", "target": "self", "script": "if(!confirm('确认删除 ' + decodeURIComponent(" + json.dumps(f["file_name"]) + ") + ' ? 不可恢复'))return;fetch(" + json.dumps(del_url) + ").then(function(r){return r.json()}).then(function(){var u='/rag/knowledge_bases_list/file_list.dspy?_webbricks_=1&kb_id='+encodeURIComponent(" + json.dumps(kb_id) + ")+'&id='+encodeURIComponent(" + json.dumps(folder_id) + ")+'&label='+encodeURIComponent(" + json.dumps(folder_label) + ");fetch(u).then(function(r){return r.json()}).then(function(d){var fp=bricks.getWidgetById('file_list_panel',bricks.app.root);if(fp){fp.clear_widgets();bricks.widgetBuild(d,fp).then(function(w){if(w)fp.add_widget(w)})}})})"}]} diff --git a/wwwroot/knowledge_bases_list/media_cards.dspy b/wwwroot/knowledge_bases_list/media_cards.dspy new file mode 100644 index 0000000..848ace5 --- /dev/null +++ b/wwwroot/knowledge_bases_list/media_cards.dspy @@ -0,0 +1,110 @@ +import json + +ns = params_kw.copy() +kb_id = ns.get('kb_id', '') +kind = ns.get('kind', 'face') + +db = DBPools() +dbname = get_module_dbname('rag') + +rows = [] +try: + async with db.sqlorContext(dbname) as sor: + if kind == 'voice': + recs = await sor.sqlExe( + "SELECT id, file_name, file_path, metadata, created_at FROM documents " + "WHERE kb_id=${kb_id}$ AND (LOWER(file_name) LIKE '%.mp3' OR LOWER(file_name) LIKE '%.wav' " + "OR LOWER(file_name) LIKE '%.m4a' OR LOWER(file_name) LIKE '%.aac' OR LOWER(file_name) LIKE '%.ogg' " + "OR LOWER(file_name) LIKE '%.flac') ORDER BY created_at DESC", + {"kb_id": kb_id}) + else: + recs = await sor.sqlExe( + "SELECT id, file_name, file_path, metadata, created_at FROM documents " + "WHERE kb_id=${kb_id}$ AND (LOWER(file_name) LIKE '%.jpg' OR LOWER(file_name) LIKE '%.jpeg' " + "OR LOWER(file_name) LIKE '%.png' OR LOWER(file_name) LIKE '%.gif' OR LOWER(file_name) LIKE '%.webp' " + "OR LOWER(file_name) LIKE '%.bmp') ORDER BY created_at DESC", + {"kb_id": kb_id}) + for r in recs: + rows.append({ + "id": r.id, + "file_name": r.file_name or '', + "file_path": r.file_path or '', + "metadata": r.metadata or '{}', + "created_at": str(r.created_at)[:16] if r.created_at else '' + }) +except Exception: + return {"widgettype": "Text", "options": {"text": "加载失败", "cfontsize": 14, "color": "#e74c3c", "halign": "center", "marginTop": "40px"}} + +def safe_url(p): + if not p: + return '' + if p.startswith('/idfile'): + return p + return '/idfile' + p + +kind_cn = '人脸' if kind != 'voice' else '声纹' + +cards = [] +for f in rows: + doc_id = f["id"] + fname = f["file_name"] + media_url = safe_url(f["file_path"]) + meta = {} + try: + meta = json.loads(f["metadata"]) + except Exception: + meta = {} + tags = meta.get('tags', []) + if not isinstance(tags, list): + tags = [] + + if kind == 'voice': + media_widget = {"widgettype": "Html", "options": {"html": "", "padding": "8px 12px"}} + else: + media_widget = {"widgettype": "Image", "options": {"url": media_url, "width": "100%", "cheight": 12, "objectFit": "contain", "bgcolor": "#f5f5f5"}} + + if tags: + tag_widgets = [] + for t in tags: + tag_widgets.append({"widgettype": "Text", "options": {"text": "#" + str(t), "cfontsize": 10, "color": "#1565c0", "bgcolor": "#e3f2fd", "padding": "1px 8px", "borderRadius": "8px", "margin": "2px 4px 2px 0"}}) + tag_row = {"widgettype": "HBox", "options": {"wrap": True, "padding": "0 12px", "alignItems": "center"}, "subwidgets": tag_widgets} + else: + tag_row = {"widgettype": "Text", "options": {"text": "暂无标签", "cfontsize": 10, "color": "#bbb", "padding": "0 12px"}} + + tag_url = "./tag_form.dspy?doc_id=" + doc_id + "&kb_id=" + kb_id + "&kind=" + kind + + cards.append({ + "widgettype": "VBox", + "options": {"css": "card", "cwidth": 20, "padding": "0", "spacing": "0", "overflow": "hidden"}, + "subwidgets": [ + media_widget, + {"widgettype": "Text", "options": {"text": fname, "cfontsize": 12, "fontWeight": "bold", "color": "#333", "padding": "8px 12px 2px 12px", "wrap": True, "halign": "left"}}, + {"widgettype": "Text", "options": {"text": f["created_at"], "cfontsize": 10, "color": "#999", "padding": "0 12px", "halign": "left"}}, + tag_row, + {"widgettype": "HBox", "options": {"padding": "8px 12px", "alignItems": "center"}, "subwidgets": [ + {"widgettype": "Text", "options": {"text": "", "css": "filler"}}, + {"widgettype": "Button", "options": {"label": "🏷 添加标签", "cfontsize": 11, "bgcolor": "#e3f2fd", "color": "#1565c0", "padding": "3px 10px", "borderRadius": "12px"}, + "binds": [{"wid": "self", "event": "click", "actiontype": "urlwidget", "target": "PopupWindow", + "popup_options": {"title": "为「" + fname + "」添加标签", "width": "34%", "height": "auto", + "movable": True, "resizable": True, "modal": True}, + "options": {"url": tag_url}}]} + ]} + ] + }) + +if not cards: + cards.append({"widgettype": "Text", "options": {"text": "该知识库暂无" + kind_cn + "文件,请先在文件列表上传对应图片/音频", "cfontsize": 13, "color": "#aaa", "halign": "center", "marginTop": "60px"}}) + +result = { + "widgettype": "VScrollPanel", + "id": "media_card_panel", + "options": {"css": "filler", "width": "100%", "height": "100%", "padding": "12px"}, + "subwidgets": [ + { + "widgettype": "DynamicColumn", + "options": {"width": "100%", "col_cwidth": 22, "col_cgap": 1}, + "subwidgets": cards + } + ] +} +return result diff --git a/wwwroot/knowledge_bases_list/tag_form.dspy b/wwwroot/knowledge_bases_list/tag_form.dspy new file mode 100644 index 0000000..6e966b3 --- /dev/null +++ b/wwwroot/knowledge_bases_list/tag_form.dspy @@ -0,0 +1,82 @@ +import json + +ns = params_kw.copy() +doc_id = ns.get('doc_id', '') +kb_id = ns.get('kb_id', '') +kind = ns.get('kind', '') + +db = DBPools() +dbname = get_module_dbname('rag') + +fname = '' +cur_tags = [] +try: + async with db.sqlorContext(dbname) as sor: + recs = await sor.sqlExe( + "SELECT file_name, metadata FROM documents WHERE id=${id}$ AND kb_id=${kb_id}$", + {"id": doc_id, "kb_id": kb_id}) + if recs: + fname = recs[0].file_name or '' + try: + meta = json.loads(recs[0].metadata or '{}') + except Exception: + meta = {} + cur_tags = meta.get('tags', []) + if not isinstance(cur_tags, list): + cur_tags = [] +except Exception: + pass + +subwidgets = [] +if not doc_id: + subwidgets.append({"widgettype": "Text", "options": {"text": "缺少 doc_id 参数", "cfontsize": 13, "color": "#e74c3c", "padding": "20px"}}) +else: + subwidgets.append({"widgettype": "Text", "options": {"text": "📄 " + (fname or doc_id), "cfontsize": 13, "fontWeight": "bold", "color": "#333", "padding": "0 0 8px 0"}}) + if cur_tags: + tws = [] + for t in cur_tags: + tws.append({"widgettype": "Text", "options": {"text": "#" + str(t), "cfontsize": 10, "color": "#1565c0", "bgcolor": "#e3f2fd", "padding": "1px 8px", "borderRadius": "8px", "margin": "2px 6px 2px 0"}}) + subwidgets.append({"widgettype": "HBox", "options": {"wrap": True, "alignItems": "center", "marginBottom": "8px"}, "subwidgets": tws}) + else: + subwidgets.append({"widgettype": "Text", "options": {"text": "暂无标签", "cfontsize": 11, "color": "#aaa", "marginBottom": "8px"}}) + subwidgets.append({ + "widgettype": "Form", + "id": "tag_input_form", + "options": { + "cols": 1, + "fields": [ + {"name": "tag", "label": "标签名", "uitype": "str", "required": True} + ], + "buttons": [{"name": "submit", "label": "✓ 添加标签", "icon": "save"}] + } + }) + +refresh_js = '' +if kind: + refresh_js = ("var mp=document.querySelector('#media_card_panel');" + "if(mp&&mp.bricks_widget){fetch('/rag/knowledge_bases_list/media_cards.dspy?_webbricks_=1&kb_id=" + + kb_id + "&kind=" + kind + "').then(function(r){return r.json()}).then(function(d){" + "var mw=mp.bricks_widget;mw.clear_widgets();bricks.widgetBuild(d,mw).then(function(nw){if(nw)mw.add_widget(nw)})}).catch(function(){})};") + +tag_script = ("var d=event&&event.params?event.params:{};if(!d||!d.tag){alert('请输入标签名');return};" + "var u='/rag/knowledge_bases_list/add_tag.dspy?doc_id=" + doc_id + "&kb_id=" + kb_id + "'+'&tag='+encodeURIComponent(d.tag);" + "fetch(u).then(function(r){return r.text()}).then(function(t){var d2={};try{d2=JSON.parse(t)}catch(e){};" + "if(d2&&d2.status==='SUCCEEDED'){" + refresh_js + + "var pw=null;var w=this;while(w){if(w instanceof bricks.PopupWindow||w instanceof bricks.Popup){pw=w;break};w=w.parent};" + "if(pw){pw.dismiss();pw.destroy()}else{alert('标签已添加:'+d2.added)}}" + "else{alert('添加失败:'+((d2&&d2.error)||t))}}.bind(this)).catch(function(){alert('网络错误')})") + +result = { + "widgettype": "VBox", + "options": {"width": "100%", "padding": "16px", "spacing": "6px"}, + "subwidgets": subwidgets, + "binds": [ + { + "wid": "tag_input_form", + "event": "submit", + "actiontype": "script", + "script": tag_script + } + ] +} +return result