From 0d6acfb4ef6a5ba820c3fb8a78476c71796d887c Mon Sep 17 00:00:00 2001 From: yumoqing Date: Fri, 31 Jul 2026 14:58:21 +0800 Subject: [PATCH] feat: analysis cards with face/voiceprint results + tag system --- wwwroot/knowledge_bases_list/add_tag.dspy | 36 ++++++++ wwwroot/knowledge_bases_list/analysis.dspy | 97 +++++++++++++++++++++ wwwroot/knowledge_bases_list/file_list.dspy | 5 +- 3 files changed, 137 insertions(+), 1 deletion(-) create mode 100644 wwwroot/knowledge_bases_list/add_tag.dspy create mode 100644 wwwroot/knowledge_bases_list/analysis.dspy diff --git a/wwwroot/knowledge_bases_list/add_tag.dspy b/wwwroot/knowledge_bases_list/add_tag.dspy new file mode 100644 index 0000000..318a100 --- /dev/null +++ b/wwwroot/knowledge_bases_list/add_tag.dspy @@ -0,0 +1,36 @@ +ns = params_kw.copy() +doc_id = ns.get('doc_id', '') +tag = ns.get('tag', '').strip() +kb_id = ns.get('kb_id', '') +env = request._run_ns + +if not doc_id or not tag: + return json.dumps({"status": "error", "error": "doc_id and tag required"}, ensure_ascii=False) + +async with get_sor_context(env, 'rag') as sor: + # Get existing tags + recs = await sor.sqlExe("SELECT metadata FROM documents WHERE id=${id}$ AND kb_id=${kb_id}$", + {"id": doc_id, "kb_id": kb_id}) + if not recs: + return json.dumps({"status": "error", "error": "document not found"}, ensure_ascii=False) + + import json as _json + meta = {} + try: + meta = _json.loads(recs[0].metadata or '{}') + except: + meta = {} + + tags = meta.get('tags', []) + if not isinstance(tags, list): + tags = [] + + if tag not in tags: + tags.append(tag) + meta['tags'] = tags + await sor.sqlExe( + "UPDATE documents SET metadata=${meta}$ WHERE id=${id}$", + {"meta": _json.dumps(meta, ensure_ascii=False), "id": doc_id}) + return _json.dumps({"status": "SUCCEEDED", "tags": tags, "added": tag}, ensure_ascii=False) + else: + return _json.dumps({"status": "SUCCEEDED", "tags": tags, "duplicate": True}, ensure_ascii=False) diff --git a/wwwroot/knowledge_bases_list/analysis.dspy b/wwwroot/knowledge_bases_list/analysis.dspy new file mode 100644 index 0000000..68cc833 --- /dev/null +++ b/wwwroot/knowledge_bases_list/analysis.dspy @@ -0,0 +1,97 @@ +ns = params_kw.copy() +kb_id = ns.get('kb_id', '') +env = request._run_ns + +rows = [] +async with get_sor_context(env, 'rag') as sor: + recs = await sor.sqlExe( + "SELECT id, file_name, file_size, file_type, file_path, metadata, status, chunk_count " + "FROM documents WHERE kb_id=${kb_id}$ AND metadata IS NOT NULL AND metadata != '' " + "ORDER BY created_at DESC LIMIT 50", + {"kb_id": kb_id}) + rows = [dict(r) for r in recs] + +cards = [] + +import json as _json + +for r in rows: + meta = {} + try: + meta = _json.loads(r['metadata'] or '{}') + except: + pass + face_count = meta.get('face', meta.get('faces', 0)) or 0 + speakers = meta.get('speakers', meta.get('voiceprint', 0)) or 0 + + # Build card for this document + ext = (r['file_name'] or '').rsplit('.', 1)[-1].lower() if '.' in (r['file_name'] or '') else '' + icon = '📄' + if ext in ('mp4','avi','mov','mkv','webm'): icon = '🎬' + elif ext in ('jpg','jpeg','png','gif','bmp','webp'): icon = '🖼' + elif ext in ('mp3','wav','flac','ogg','m4a'): icon = '🎵' + elif ext in ('pdf',): icon = '📕' + elif ext in ('docx','doc'): icon = '📘' + elif ext in ('pptx','ppt'): icon = '📙' + elif ext in ('xlsx','xls'): icon = '📊' + + badges = [] + if face_count > 0: + badges.append('😀×' + str(int(face_count))) + if speakers > 0: + badges.append('🎤×' + str(int(speakers))) + badge_text = ' '.join(badges) if badges else '暂无识别' + + cards.append({ + "widgettype": "VBox", + "options": {"width": "calc(50% - 8px)", "padding": "14px", "marginBottom": "12px", + "border": "1px solid #e0e0e0", "borderRadius": "8px", "bgcolor": "#fff", + "css": "card", "display": "inline-block", "verticalAlign": "top"}, + "subwidgets": [ + {"widgettype": "HBox", "options": {"alignItems": "center", "marginBottom": "8px"}, "subwidgets": [ + {"widgettype": "Text", "options": {"text": icon + " " + (r['file_name'] or '')[:40], + "cfontsize": 14, "fontWeight": "bold", "color": "#333", "cwidth": 0, "css": "filler"}} + ]}, + {"widgettype": "HBox", "options": {"spacing": "8px", "marginBottom": "10px"}, "subwidgets": [ + {"widgettype": "Text", "options": {"text": badge_text, "cfontsize": 12, + "bgcolor": "#e8f5e9" if face_count or speakers else "#f5f5f5", + "color": "#2e7d32" if face_count or speakers else "#999", + "padding": "4px 10px", "borderRadius": "12px"}} + ]}, + {"widgettype": "HBox", "options": {"spacing": "6px"}, "subwidgets": [ + {"widgettype": "Text", "options": {"text": "😀 人脸: " + str(int(face_count)), + "cfontsize": 12, "color": "#666"}}, + {"widgettype": "Text", "options": {"text": "🎤 声纹: " + str(int(speakers)), + "cfontsize": 12, "color": "#666", "marginLeft": "12px"}} + ]}, + {"widgettype": "Text", "options": {"text": "", "cheight": 0.3}}, + {"widgettype": "Button", "options": { + "label": "🏷 添加标签", + "cfontsize": 12, "bgcolor": "#f0f0f0", "color": "#666", + "css": "tag-btn", "padding": "4px 12px", "borderRadius": "16px", + "border": "1px solid #ddd" + }, "binds": [{ + "wid": "self", "event": "click", "actiontype": "script", + "target": "self", + "script": "var doc_id='" + r['id'] + "';var tag=prompt('请输入标签:');if(!tag)return;var u='/rag/knowledge_bases_list/add_tag.dspy?doc_id='+encodeURIComponent(doc_id)+'&tag='+encodeURIComponent(tag)+'&kb_id=" + kb_id + "';fetch(u).then(function(r){return r.json()}).then(function(d){if(d.status==='SUCCEEDED'){alert('标签已添加');window.location.reload()}else{alert('失败:'+JSON.stringify(d))}})" + }]} + ] + }) + +if not cards: + return json.dumps({ + "widgettype": "Text", "options": { + "text": "暂无分析结果", "cfontsize": 14, "color": "#aaa", "halign": "center", "marginTop": "40px" + } + }, ensure_ascii=False) + +# Wrap cards in FlexBox-like container +return json.dumps({ + "widgettype": "VBox", + "options": {"padding": "20px", "spacing": "8px"}, + "subwidgets": [ + {"widgettype": "Text", "options": {"text": "📊 文件分析结果", "cfontsize": 20, "fontWeight": "bold", "color": "#1a1a2e", "marginBottom": "16px"}}, + {"widgettype": "Text", "options": {"text": "人脸检测 & 声纹识别 | 共 " + str(len(cards)) + " 个文件", "cfontsize": 13, "color": "#888", "marginBottom": "16px"}}, + {"widgettype": "VBox", "options": {"width": "100%"}, "subwidgets": cards} + ] +}, ensure_ascii=False) diff --git a/wwwroot/knowledge_bases_list/file_list.dspy b/wwwroot/knowledge_bases_list/file_list.dspy index d554fd0..8397308 100644 --- a/wwwroot/knowledge_bases_list/file_list.dspy +++ b/wwwroot/knowledge_bases_list/file_list.dspy @@ -50,7 +50,10 @@ header = { "subwidgets": [ {"widgettype": "Text", "options": {"text": "📄 " + folder_label, "cfontsize": 14, "fontWeight": "bold", "color": "#333"}}, {"widgettype": "Text", "options": {"text": "(" + str(len(rows)) + " 个文件)", "cfontsize": 12, "color": "#999", "marginLeft": "8px"}}, - {"widgettype": "Text", "options": {"text": "", "css": "filler"}} + {"widgettype": "Text", "options": {"text": "", "css": "filler"}}, + {"widgettype": "Button", "options": {"label": "📊 分析结果", "cfontsize": 12, "bgcolor": "#ede7f6", "color": "#4527a0", "padding": "4px 12px", "borderRadius": "14px", "marginLeft": "8px"}, + "binds": [{"wid": "self", "event": "click", "actiontype": "urlwidget", "target": "file_list_panel", "mode": "replace", + "options": {"url": "./analysis.dspy?kb_id=" + kb_id}}]} ] }