Files
FabledScribe/frontend/src/components/DiffView.vue
T
bvandeusen 48f070f773 Project-aware assist, link suggestions, project-scoped RAG, semantic search tool, SSE race fix
- Writing assistant: inject project notes as context (definition-tagged first), wikilink suggestions
- Link suggestions: server-side endpoint finds unlinked term occurrences, NoteEditorView sidebar panel
- Project-scoped RAG: ChatView ProjectSelector filters semantic+keyword search to selected project
- Semantic search tool: LLM search_notes upgraded to hybrid semantic (0.40 threshold) + keyword merge
- SSE race condition fix: drain remaining events after stream loop exits in chat.py and notes.py
- RAG_AUTO_SNIPPET raised 800→4000; sidebar include uses full note body; MAX_BODY_CHARS 8000→24000
- Enter-to-submit on writing assistant instruction textareas (note and task editors)
- DiffView: equal-line collapsing with 3-line context around changes

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-06 14:02:54 -05:00

177 lines
4.4 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<script setup lang="ts">
import { computed } from "vue";
import type { DiffLine } from "@/composables/useAssist";
const props = defineProps<{
diff: DiffLine[];
}>();
const CONTEXT_LINES = 3;
const MIN_COLLAPSE = 6; // Only collapse if there are more than this many consecutive equal lines
type CollapseMarker = { type: 'collapse'; count: number };
type DisplayItem = DiffLine | CollapseMarker;
const insertions = computed(() => props.diff.filter(l => l.type === 'insert').length);
const deletions = computed(() => props.diff.filter(l => l.type === 'delete').length);
const displayItems = computed<DisplayItem[]>(() => {
const d = props.diff;
if (!d.length) return [];
// Mark which indices are within CONTEXT_LINES of a change
const shown = new Set<number>();
for (let i = 0; i < d.length; i++) {
if (d[i].type !== 'equal') {
for (let c = Math.max(0, i - CONTEXT_LINES); c <= Math.min(d.length - 1, i + CONTEXT_LINES); c++) {
shown.add(c);
}
}
}
// If nothing changed, show all
if (shown.size === 0) return d;
const result: DisplayItem[] = [];
let i = 0;
while (i < d.length) {
if (shown.has(i)) {
result.push(d[i]);
i++;
} else {
let count = 0;
while (i < d.length && !shown.has(i)) { count++; i++; }
if (count >= MIN_COLLAPSE) {
result.push({ type: 'collapse', count });
} else {
// Too short to collapse — show as-is
for (let k = i - count; k < i; k++) result.push(d[k]);
}
}
}
return result;
});
function markerFor(type: DiffLine['type']): string {
if (type === 'insert') return '+';
if (type === 'delete') return '';
return ' ';
}
</script>
<template>
<div class="diff-view-full">
<div class="diff-summary-bar">
<span class="diff-summary-ins">+{{ insertions }} insertion{{ insertions !== 1 ? 's' : '' }}</span>
<span class="diff-summary-del">{{ deletions }} deletion{{ deletions !== 1 ? 's' : '' }}</span>
</div>
<div class="diff-scroll">
<div v-if="!diff.length" class="diff-empty">No changes.</div>
<template v-for="(item, i) in displayItems" :key="i">
<div v-if="item.type === 'collapse'" class="diff-line diff-collapse">
<span class="diff-marker"></span>
<span class="diff-text">{{ item.count }} unchanged line{{ item.count !== 1 ? 's' : '' }}</span>
</div>
<div
v-else
:class="['diff-line', `diff-${item.type}`]"
>
<span class="diff-marker">{{ markerFor(item.type) }}</span>
<span class="diff-text">{{ item.text }}</span>
</div>
</template>
</div>
</div>
</template>
<style scoped>
.diff-view-full {
display: flex;
flex-direction: column;
flex: 1;
min-height: 0;
border: 1px solid var(--color-input-border);
border-radius: var(--radius-sm);
background: var(--color-bg);
overflow: hidden;
}
.diff-summary-bar {
position: sticky;
top: 0;
flex-shrink: 0;
display: flex;
gap: 1rem;
padding: 0.4rem 0.75rem;
background: var(--color-bg-secondary);
border-bottom: 1px solid var(--color-border);
font-size: 0.78rem;
font-family: monospace;
font-weight: 600;
}
.diff-summary-ins { color: var(--color-success, #2ecc71); }
.diff-summary-del { color: var(--color-danger, #e74c3c); }
.diff-scroll {
flex: 1;
min-height: 0;
overflow-y: auto;
padding: 0.3rem 0;
}
.diff-empty {
padding: 0.75rem;
font-size: 0.85rem;
color: var(--color-text-muted);
font-style: italic;
}
.diff-line {
display: flex;
align-items: baseline;
padding: 0.04rem 0.5rem;
line-height: 1.55;
font-family: monospace;
font-size: 0.82rem;
}
.diff-delete {
background: color-mix(in srgb, var(--color-danger, #e74c3c) 12%, transparent);
color: var(--color-danger, #e74c3c);
}
.diff-insert {
background: color-mix(in srgb, var(--color-success, #2ecc71) 12%, transparent);
color: var(--color-success, #2ecc71);
}
.diff-equal {
color: var(--color-text-muted);
}
.diff-collapse {
color: var(--color-text-muted);
opacity: 0.6;
font-style: italic;
border-top: 1px dashed var(--color-border);
border-bottom: 1px dashed var(--color-border);
padding-top: 0.2rem;
padding-bottom: 0.2rem;
}
.diff-marker {
flex-shrink: 0;
width: 1rem;
text-align: center;
user-select: none;
font-weight: 700;
}
.diff-text {
flex: 1;
white-space: pre-wrap;
word-break: break-word;
}
</style>