feat: Web-Search Sources anzeigen (Streaming + History + Archive + Shared)

Backend:
- web_search_tool_result Sources extrahieren (url, title)
- Als 'web_search_result' Message-Typ in DB persistieren
- Sources im web_search_end SSE-Event mitsenden
- Alle 3 Chat-Pfade: StreamChat, RunAgentChat, ChatNonStreaming

Frontend:
- renderToolBoxContent: Sources-Sektion mit klickbaren Domain-Links
- finishToolBox/addToolBoxStatic: sources-Parameter durchgereicht
- History: web_search_result Message-Typ in allen 3 Render-Pfaden
- web_search immer sichtbar (auch bei 'modifications' Filter)

CSS:
- .web-search-sources: Flex-Wrap Container
- .web-search-source: Domain-Pill mit Hover-Effekt, Title-Ellipsis
This commit is contained in:
Meleki committed 2026-03-07 09:56:09 +01:00
1 parent 116f21105c
commit f64052caf4
5 files changed
+221 -7

No files matched your search

+49
View File
@@ -170,6 +170,55 @@
padding: var(--badge-padding);
}
/* Web Search Sources */
.web-search-sources {
display: flex;
flex-wrap: wrap;
gap: 0.4em;
padding: var(--box-padding);
padding-top: 0;
}
.web-search-source {
display: inline-flex;
align-items: center;
gap: 0.3em;
padding: 0.2em 0.6em;
background: var(--f2-bg);
border: var(--border-width) solid var(--f2-border);
border-radius: var(--border-radius);
color: var(--text) !important;
text-decoration: none !important;
font-size: 0.85em;
transition: background var(--transition-speed), border-color var(--transition-speed);
max-width: 20em;
overflow: hidden;
}
.web-search-source:hover {
background: var(--primary);
border-color: var(--primary);
color: var(--primary-text) !important;
}
.web-search-source .fa {
font-size: 0.75em;
opacity: 0.6;
}
.source-domain {
font-weight: 600;
white-space: nowrap;
}
.source-title {
opacity: 0.7;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
/* Diff View */
.tool-diff {
+1 -1
View File
@@ -210,7 +210,7 @@ async function sendMessage() {
case 'web_search_end':
if (currentToolBox) {
finishToolBox(currentToolBox, { name: 'web_search', result: 'Suche abgeschlossen', isError: false });
finishToolBox(currentToolBox, { name: 'web_search', result: 'Suche abgeschlossen', isError: false, sources: data.sources });
}
currentToolBox = null;
_afterWebSearch = true;
+28
View File
@@ -248,6 +248,16 @@ function _renderMessagesIntoFragment(fragment, msgs, imageMessageIds) {
renderToolBoxContent(div, msg.toolName, msg.content, null, false);
fragment.appendChild(div);
break;
case 'web_search_result': {
if (!isToolVisible('web_search')) continue;
div.className = 'tool-box';
div.dataset.toolName = 'web_search';
var wsSources = [];
try { wsSources = JSON.parse(msg.content || '[]'); } catch(e) {}
renderToolBoxContent(div, 'web_search', null, null, false, wsSources);
fragment.appendChild(div);
break;
}
case 'tool_result': {
const prevBoxes = fragment.querySelectorAll('.tool-box');
const lastToolBox = prevBoxes[prevBoxes.length - 1];
@@ -304,6 +314,15 @@ function renderMessages(msgs, addToConversation) {
}
break;
case 'web_search_result': {
if (isToolVisible('web_search')) {
var wsSrc = [];
try { wsSrc = JSON.parse(msg.content || '[]'); } catch(e) {}
addToolBoxStatic('web_search', null, null, false, wsSrc);
}
break;
}
case 'tool_result':
// tool_result wird dem vorherigen tool_use-Box angehängt wenn möglich
appendToolResult(msg.content, msg.isError);
@@ -400,6 +419,15 @@ async function loadLatestArchive() {
div.className = 'tool-box archived';
renderToolBoxContent(div, msg.toolName, msg.content, null, false);
break;
case 'web_search_result': {
if (!isToolVisible('web_search')) continue;
div.className = 'tool-box archived';
div.dataset.toolName = 'web_search';
var archSrc = [];
try { archSrc = JSON.parse(msg.content || '[]'); } catch(e) {}
renderToolBoxContent(div, 'web_search', null, null, false, archSrc);
break;
}
case 'tool_result': {
// An vorherigen tool-box anhängen
const prevBoxes = fragment.querySelectorAll ? fragment.querySelectorAll('.tool-box') : [];
+25 -5
View File
@@ -823,7 +823,8 @@ function isToolVisible(toolName) {
const mode = loadSettings().toolCallDisplay;
if (mode === 'all') return true;
if (mode === 'hidden') return false;
// 'modifications' – nur modifizierende Tools anzeigen
// 'modifications' – nur modifizierende Tools + web_search anzeigen
if (toolName === 'web_search') return true;
return MODIFICATION_TOOLS.includes(toolName);
}
@@ -855,7 +856,7 @@ function addToolBoxLoading(name) {
return div;
}
function renderToolBoxContent(div, name, inputJson, result, isError) {
function renderToolBoxContent(div, name, inputJson, result, isError, sources) {
let parsedInput = null;
try { parsedInput = JSON.parse(inputJson); } catch {}
@@ -879,6 +880,25 @@ function renderToolBoxContent(div, name, inputJson, result, isError) {
let foldTarget = null;
// Web-Search Sources – immer sichtbar, kein Toggle
if (name === 'web_search' && sources && sources.length > 0) {
const sourcesList = document.createElement('div');
sourcesList.className = 'web-search-sources';
sources.forEach(function(s) {
const link = document.createElement('a');
link.href = s.url;
link.target = '_blank';
link.rel = 'noopener noreferrer';
link.className = 'web-search-source';
var domain = '';
try { domain = new URL(s.url).hostname.replace('www.', ''); } catch(e) { domain = s.url; }
link.innerHTML = '<i class="fa fa-external-link"></i> <span class="source-domain">' + esc(domain) + '</span>' +
(s.title ? '<span class="source-title">' + esc(s.title) + '</span>' : '');
sourcesList.appendChild(link);
});
div.appendChild(sourcesList);
}
if (name === 'edit_file' && parsedInput && parsedInput.path) {
const pathSpan = document.createElement('span');
pathSpan.className = 'tool-input';
@@ -1072,15 +1092,15 @@ function buildTodoList(result, isError) {
function finishToolBox(div, data) {
if (!div) return;
renderToolBoxContent(div, data.name, data.input, data.result, data.isError);
renderToolBoxContent(div, data.name, data.input, data.result, data.isError, data.sources);
scrollDown();
}
function addToolBoxStatic(name, input, result, isError) {
function addToolBoxStatic(name, input, result, isError, sources) {
const div = document.createElement('div');
div.className = 'tool-box';
div.dataset.toolName = name;
renderToolBoxContent(div, name, input, result, isError);
renderToolBoxContent(div, name, input, result, isError, sources);
chatEl().appendChild(div);
scrollDown();
}