Add web search knowledge base, message tagging, and word-memory tip topics

Introduces websearch.py (Tavily) + knowledge.py to ground chat responses
in search results, tagging.py to tag user and assistant messages via LLM
and pick related topics for the Discord useless-tips job, and
word_memory.py to track recent conversation nouns (surfaced in the new
WordPool.vue sidebar) used to pick tip topics close to a related tag.
This commit is contained in:
2026-06-20 18:06:06 +09:00
parent 4e837d1419
commit 234ebba103
23 changed files with 946 additions and 39 deletions
+98 -10
View File
@@ -2,10 +2,20 @@
import { ref, watch, nextTick } from 'vue'
import { useChatStore } from '../stores/chat'
import MarkdownContent from './MarkdownContent.vue'
import type { Source } from '../api/client'
const store = useChatStore()
const scrollEl = ref<HTMLElement | null>(null)
function uniqueSources(sources: Source[]): Source[] {
const seen = new Set<string>()
return sources.filter((s) => {
if (seen.has(s.url)) return false
seen.add(s.url)
return true
})
}
watch(
() => store.messages.length,
async () => {
@@ -23,17 +33,36 @@ watch(
<div
v-for="(msg, i) in store.messages"
:key="i"
:class="['message', msg.role, { error: msg.error }]"
:class="['message-wrap', msg.role]"
>
<div class="bubble">
<!-- ユーザーメッセージはプレーンテキスト -->
<template v-if="msg.role === 'user'">{{ msg.content }}</template>
<div :class="['message', msg.role, { error: msg.error }]">
<div class="bubble">
<!-- ユーザーメッセージはプレーンテキスト -->
<template v-if="msg.role === 'user'">{{ msg.content }}</template>
<!-- アシスタントはMarkdownレンダリング -->
<template v-else>
<MarkdownContent :content="msg.content" :pending="msg.pending" />
<span v-if="msg.pending && !msg.content" class="cursor">▌</span>
</template>
<!-- アシスタントはMarkdownレンダリング -->
<template v-else>
<MarkdownContent :content="msg.content" :pending="msg.pending" />
<span v-if="msg.pending && !msg.content" class="cursor">▌</span>
<button v-if="msg.error" class="retry-btn" :disabled="store.isStreaming" @click="store.retryMessage(i)">
🔄 再送信
</button>
</template>
</div>
</div>
<div v-if="msg.tags && msg.tags.length > 0" class="tags">
<span v-for="tag in msg.tags" :key="tag" class="tag">#{{ tag }}</span>
</div>
<div v-if="msg.sources && msg.sources.length > 0" class="sources">
<span class="sources-label">🔍 参照元:</span>
<a
v-for="src in uniqueSources(msg.sources)"
:key="src.url"
:href="src.url"
target="_blank"
rel="noopener noreferrer"
class="source-link"
>{{ src.title || src.url }}</a>
</div>
</div>
</div>
@@ -53,9 +82,50 @@ watch(
color: #555;
font-size: 14px;
}
.message { display: flex; }
.message-wrap { display: flex; flex-direction: column; gap: 4px; }
.message-wrap.user { align-items: flex-end; }
.message-wrap.assistant { align-items: flex-start; }
.message { display: flex; width: 100%; }
.message.user { justify-content: flex-end; }
.message.assistant { justify-content: flex-start; }
.tags {
display: flex;
flex-wrap: wrap;
gap: 6px;
max-width: 72%;
}
.tag {
font-size: 11px;
color: #93c5fd;
background: #1d3557;
border-radius: 10px;
padding: 2px 8px;
}
.sources {
display: flex;
flex-wrap: wrap;
align-items: center;
gap: 6px;
max-width: 72%;
font-size: 11px;
}
.sources-label {
color: #888;
}
.source-link {
color: #6ee7b7;
background: #103027;
border-radius: 10px;
padding: 2px 8px;
text-decoration: none;
max-width: 220px;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.source-link:hover {
text-decoration: underline;
}
.bubble {
max-width: 72%;
padding: 10px 14px;
@@ -84,5 +154,23 @@ watch(
display: inline-block;
animation: blink 0.8s step-end infinite;
}
.retry-btn {
display: block;
margin-top: 8px;
background: #2a1414;
color: #f87171;
border: 1px solid #7f1d1d;
border-radius: 6px;
padding: 4px 10px;
font-size: 12px;
cursor: pointer;
}
.retry-btn:hover:not(:disabled) {
background: #3b1a1a;
}
.retry-btn:disabled {
opacity: 0.5;
cursor: not-allowed;
}
@keyframes blink { 50% { opacity: 0; } }
</style>