변경3
This commit is contained in:
@@ -53,54 +53,75 @@
|
||||
<div class="message-content">
|
||||
<div class="message-text" v-html="renderMarkdown(msg.content)"></div>
|
||||
|
||||
<!-- 참조 문서 (개선된 버전) -->
|
||||
<!-- 참조 문서 (문서별 그룹핑) -->
|
||||
<div v-if="msg.sources && msg.sources.length > 0" class="message-sources">
|
||||
<div
|
||||
class="sources-toggle"
|
||||
@click="msg.sourcesExpanded = !msg.sourcesExpanded"
|
||||
>
|
||||
<span class="toggle-icon">{{ msg.sourcesExpanded ? '▼' : '▶' }}</span>
|
||||
<span class="toggle-text">📎 참조 문서 ({{ msg.sources.length }}개)</span>
|
||||
<span class="toggle-text">
|
||||
📎 참조 문서 ({{ getUniqueDocCount(msg.sources) }}개 문서, {{ msg.sources.length }}개 구간)
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<transition name="slide">
|
||||
<div v-if="msg.sourcesExpanded" class="sources-list">
|
||||
<!-- 문서별로 그룹핑 -->
|
||||
<div
|
||||
v-for="(source, idx) in msg.sources"
|
||||
:key="idx"
|
||||
class="source-card"
|
||||
v-for="(group, docId) in groupSourcesByDoc(msg.sources)"
|
||||
:key="docId"
|
||||
class="doc-group"
|
||||
>
|
||||
<div class="source-header">
|
||||
<div
|
||||
class="similarity-badge"
|
||||
:class="getSimilarityClass(source.similarity)"
|
||||
>
|
||||
<div class="similarity-bar" :style="{ width: (source.similarity * 100) + '%' }"></div>
|
||||
<span class="similarity-text">{{ (source.similarity * 100).toFixed(0) }}%</span>
|
||||
</div>
|
||||
<span class="source-doc" v-if="source.docName">
|
||||
📄 {{ source.docName }}
|
||||
<span class="chunk-info" v-if="source.totalChunks">
|
||||
({{ source.chunkIndex + 1 }}/{{ source.totalChunks }})
|
||||
</span>
|
||||
</span>
|
||||
<span class="file-type-badge" v-if="source.fileType">
|
||||
{{ source.fileType.toUpperCase() }}
|
||||
</span>
|
||||
</div>
|
||||
<div class="source-content">
|
||||
{{ truncateText(source.content, 200) }}
|
||||
</div>
|
||||
<button
|
||||
v-if="source.content && source.content.length > 200"
|
||||
class="expand-btn"
|
||||
@click="source.expanded = !source.expanded"
|
||||
<!-- 문서 헤더 -->
|
||||
<div
|
||||
class="doc-header"
|
||||
@click="group.expanded = !group.expanded"
|
||||
>
|
||||
{{ source.expanded ? '접기' : '더보기' }}
|
||||
</button>
|
||||
<div v-if="source.expanded" class="source-full-content">
|
||||
{{ source.content }}
|
||||
<span class="doc-toggle">{{ group.expanded ? '▼' : '▶' }}</span>
|
||||
<span class="file-type-badge" v-if="group.fileType">
|
||||
{{ group.fileType.toUpperCase() }}
|
||||
</span>
|
||||
<span class="doc-name">📄 {{ group.docName || '문서 ' + docId }}</span>
|
||||
<span class="doc-chunk-count">({{ group.chunks.length }}개 구간)</span>
|
||||
<span class="doc-max-similarity">
|
||||
최대 {{ (group.maxSimilarity * 100).toFixed(0) }}%
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<!-- 청크 목록 -->
|
||||
<transition name="slide">
|
||||
<div v-if="group.expanded" class="chunk-list">
|
||||
<div
|
||||
v-for="(chunk, idx) in group.chunks"
|
||||
:key="idx"
|
||||
class="chunk-card"
|
||||
>
|
||||
<div class="chunk-header">
|
||||
<div
|
||||
class="similarity-badge"
|
||||
:class="getSimilarityClass(chunk.similarity)"
|
||||
>
|
||||
<div class="similarity-bar" :style="{ width: (chunk.similarity * 100) + '%' }"></div>
|
||||
<span class="similarity-text">{{ (chunk.similarity * 100).toFixed(0) }}%</span>
|
||||
</div>
|
||||
<span class="chunk-position">
|
||||
구간 {{ chunk.chunkIndex + 1 }} / {{ chunk.totalChunks }}
|
||||
</span>
|
||||
</div>
|
||||
<div class="chunk-content">
|
||||
{{ chunk.expanded ? chunk.content : truncateText(chunk.content, 150) }}
|
||||
</div>
|
||||
<button
|
||||
v-if="chunk.content && chunk.content.length > 150"
|
||||
class="expand-btn"
|
||||
@click="chunk.expanded = !chunk.expanded"
|
||||
>
|
||||
{{ chunk.expanded ? '접기' : '더보기' }}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</transition>
|
||||
</div>
|
||||
</div>
|
||||
</transition>
|
||||
@@ -187,6 +208,43 @@ const startNewChat = () => {
|
||||
sessionKey.value = null
|
||||
}
|
||||
|
||||
// 고유 문서 수 계산
|
||||
const getUniqueDocCount = (sources) => {
|
||||
const uniqueDocs = new Set(sources.map(s => s.docId))
|
||||
return uniqueDocs.size
|
||||
}
|
||||
|
||||
// 문서별로 소스 그룹핑
|
||||
const groupSourcesByDoc = (sources) => {
|
||||
const groups = {}
|
||||
|
||||
sources.forEach(source => {
|
||||
const docId = source.docId
|
||||
if (!groups[docId]) {
|
||||
groups[docId] = {
|
||||
docId: docId,
|
||||
docName: source.docName,
|
||||
fileType: source.fileType,
|
||||
totalChunks: source.totalChunks,
|
||||
chunks: [],
|
||||
maxSimilarity: 0,
|
||||
expanded: true // 기본 펼침
|
||||
}
|
||||
}
|
||||
groups[docId].chunks.push({ ...source, expanded: false })
|
||||
if (source.similarity > groups[docId].maxSimilarity) {
|
||||
groups[docId].maxSimilarity = source.similarity
|
||||
}
|
||||
})
|
||||
|
||||
// 청크 정렬 (chunkIndex 순)
|
||||
Object.values(groups).forEach(group => {
|
||||
group.chunks.sort((a, b) => a.chunkIndex - b.chunkIndex)
|
||||
})
|
||||
|
||||
return groups
|
||||
}
|
||||
|
||||
// 메시지 전송
|
||||
const sendMessage = async () => {
|
||||
const question = inputText.value.trim()
|
||||
@@ -213,11 +271,11 @@ const sendMessage = async () => {
|
||||
sessionKey.value = data.sessionKey
|
||||
}
|
||||
|
||||
// AI 응답 추가 (sourcesExpanded 기본값 false)
|
||||
// AI 응답 추가
|
||||
messages.value.push({
|
||||
role: 'assistant',
|
||||
content: data.answer,
|
||||
sources: data.sources?.map(s => ({ ...s, expanded: false })) || [],
|
||||
sources: data.sources || [],
|
||||
sourcesExpanded: false
|
||||
})
|
||||
} catch (error) {
|
||||
@@ -536,7 +594,7 @@ onMounted(() => {
|
||||
}
|
||||
}
|
||||
|
||||
// 참조 문서 섹션 (개선)
|
||||
// 참조 문서 섹션
|
||||
.message-sources {
|
||||
margin-top: 16px;
|
||||
border-top: 1px solid #eee;
|
||||
@@ -559,7 +617,6 @@ onMounted(() => {
|
||||
.toggle-icon {
|
||||
font-size: 10px;
|
||||
color: #888;
|
||||
transition: transform 0.2s;
|
||||
}
|
||||
|
||||
.toggle-text {
|
||||
@@ -573,91 +630,54 @@ onMounted(() => {
|
||||
margin-top: 12px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 10px;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.source-card {
|
||||
background: linear-gradient(135deg, #f8f9fc 0%, #f0f2f8 100%);
|
||||
// 문서 그룹
|
||||
.doc-group {
|
||||
background: #f8f9fc;
|
||||
border: 1px solid #e8ebf0;
|
||||
border-radius: 12px;
|
||||
padding: 14px;
|
||||
transition: all 0.2s;
|
||||
|
||||
&:hover {
|
||||
border-color: #667eea;
|
||||
box-shadow: 0 2px 8px rgba(102, 126, 234, 0.1);
|
||||
}
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.source-header {
|
||||
.doc-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
.similarity-badge {
|
||||
position: relative;
|
||||
min-width: 60px;
|
||||
height: 24px;
|
||||
background: #e8e8e8;
|
||||
border-radius: 12px;
|
||||
overflow: hidden;
|
||||
gap: 10px;
|
||||
padding: 12px 16px;
|
||||
background: linear-gradient(135deg, #f0f2f8 0%, #e8ebf2 100%);
|
||||
cursor: pointer;
|
||||
transition: background 0.2s;
|
||||
|
||||
.similarity-bar {
|
||||
position: absolute;
|
||||
left: 0;
|
||||
top: 0;
|
||||
height: 100%;
|
||||
border-radius: 12px;
|
||||
transition: width 0.3s ease;
|
||||
&:hover {
|
||||
background: linear-gradient(135deg, #e8ebf2 0%, #dfe3ed 100%);
|
||||
}
|
||||
|
||||
.similarity-text {
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
height: 100%;
|
||||
.doc-toggle {
|
||||
font-size: 10px;
|
||||
color: #888;
|
||||
}
|
||||
|
||||
.doc-name {
|
||||
flex: 1;
|
||||
font-size: 13px;
|
||||
font-weight: 600;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
.doc-chunk-count {
|
||||
font-size: 12px;
|
||||
color: #888;
|
||||
}
|
||||
|
||||
.doc-max-similarity {
|
||||
font-size: 11px;
|
||||
font-weight: 700;
|
||||
padding: 2px 8px;
|
||||
background: linear-gradient(135deg, #11998e 0%, #38ef7d 100%);
|
||||
color: white;
|
||||
text-shadow: 0 1px 2px rgba(0, 0, 0, 0.2);
|
||||
}
|
||||
|
||||
&.high {
|
||||
.similarity-bar {
|
||||
background: linear-gradient(90deg, #11998e 0%, #38ef7d 100%);
|
||||
}
|
||||
}
|
||||
|
||||
&.medium {
|
||||
.similarity-bar {
|
||||
background: linear-gradient(90deg, #f093fb 0%, #f5576c 100%);
|
||||
}
|
||||
}
|
||||
|
||||
&.low {
|
||||
.similarity-bar {
|
||||
background: linear-gradient(90deg, #ffecd2 0%, #fcb69f 100%);
|
||||
}
|
||||
.similarity-text {
|
||||
color: #666;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.source-doc {
|
||||
font-size: 12px;
|
||||
color: #667eea;
|
||||
font-weight: 500;
|
||||
flex: 1;
|
||||
|
||||
.chunk-info {
|
||||
color: #999;
|
||||
font-weight: 400;
|
||||
margin-left: 4px;
|
||||
border-radius: 10px;
|
||||
font-weight: 600;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -671,31 +691,104 @@ onMounted(() => {
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
.source-content {
|
||||
// 청크 목록
|
||||
.chunk-list {
|
||||
padding: 12px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.chunk-card {
|
||||
background: white;
|
||||
border: 1px solid #e8e8e8;
|
||||
border-radius: 10px;
|
||||
padding: 12px;
|
||||
transition: all 0.2s;
|
||||
|
||||
&:hover {
|
||||
border-color: #667eea;
|
||||
box-shadow: 0 2px 8px rgba(102, 126, 234, 0.1);
|
||||
}
|
||||
}
|
||||
|
||||
.chunk-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.chunk-position {
|
||||
font-size: 11px;
|
||||
color: #888;
|
||||
background: #f0f0f0;
|
||||
padding: 2px 8px;
|
||||
border-radius: 8px;
|
||||
}
|
||||
|
||||
.similarity-badge {
|
||||
position: relative;
|
||||
min-width: 55px;
|
||||
height: 22px;
|
||||
background: #e8e8e8;
|
||||
border-radius: 11px;
|
||||
overflow: hidden;
|
||||
|
||||
.similarity-bar {
|
||||
position: absolute;
|
||||
left: 0;
|
||||
top: 0;
|
||||
height: 100%;
|
||||
border-radius: 11px;
|
||||
transition: width 0.3s ease;
|
||||
}
|
||||
|
||||
.similarity-text {
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
height: 100%;
|
||||
font-size: 10px;
|
||||
font-weight: 700;
|
||||
color: white;
|
||||
text-shadow: 0 1px 2px rgba(0, 0, 0, 0.2);
|
||||
}
|
||||
|
||||
&.high .similarity-bar {
|
||||
background: linear-gradient(90deg, #11998e 0%, #38ef7d 100%);
|
||||
}
|
||||
|
||||
&.medium .similarity-bar {
|
||||
background: linear-gradient(90deg, #f093fb 0%, #f5576c 100%);
|
||||
}
|
||||
|
||||
&.low {
|
||||
.similarity-bar {
|
||||
background: linear-gradient(90deg, #ffecd2 0%, #fcb69f 100%);
|
||||
}
|
||||
.similarity-text {
|
||||
color: #666;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.chunk-content {
|
||||
font-size: 13px;
|
||||
color: #555;
|
||||
line-height: 1.6;
|
||||
word-break: break-word;
|
||||
}
|
||||
|
||||
.source-full-content {
|
||||
margin-top: 10px;
|
||||
padding-top: 10px;
|
||||
border-top: 1px dashed #ddd;
|
||||
font-size: 13px;
|
||||
color: #444;
|
||||
line-height: 1.6;
|
||||
white-space: pre-wrap;
|
||||
word-break: break-word;
|
||||
}
|
||||
|
||||
.expand-btn {
|
||||
margin-top: 8px;
|
||||
padding: 4px 12px;
|
||||
font-size: 12px;
|
||||
font-size: 11px;
|
||||
color: #667eea;
|
||||
background: rgba(102, 126, 234, 0.1);
|
||||
border-radius: 12px;
|
||||
border-radius: 10px;
|
||||
transition: all 0.2s;
|
||||
|
||||
&:hover {
|
||||
@@ -719,7 +812,7 @@ onMounted(() => {
|
||||
.slide-enter-to,
|
||||
.slide-leave-from {
|
||||
opacity: 1;
|
||||
max-height: 1000px;
|
||||
max-height: 2000px;
|
||||
}
|
||||
|
||||
// 로딩 애니메이션
|
||||
|
||||
Reference in New Issue
Block a user