Task work log, inline writing assistant, task editor sidebar layout
Backend: - Migration 0021: task_logs table (FK → notes + users, CASCADE, indexed) - models/task_log.py: SQLAlchemy model with to_dict() - services/task_logs.py: CRUD with ownership checks, _UNSET sentinel for optional duration clear - routes/task_logs.py: GET/POST/PATCH/DELETE /api/tasks/<id>/logs - services/tools.py: log_work LLM tool (resolves task by title, creates log entry) - services/generation_task.py: retry assist generation up to 3× on HTTP 500 Frontend: - types/task.ts: TaskLog interface - TaskLogSection.vue: chronological work log with date+time timestamps, duration badge, inline edit, autofocus - InlineAssistPanel.vue: streaming preview + diff review rendered inline in editor column - useAssist.ts: removed chatStore.chatReady gate; toast notifications for errors - NoteEditorView.vue + TaskEditorView.vue: inline assist panel, aside restricted to idle state - TaskEditorView.vue: two-column layout (editor+log left, metadata sidebar right), body defaults to Preview, sidebarOpen accordion for mobile - editor-shared.css: .assist-active-hint style Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -12,6 +12,7 @@ import TiptapEditor from "@/components/TiptapEditor.vue";
|
||||
import TagInput from "@/components/TagInput.vue";
|
||||
import ProjectSelector from "@/components/ProjectSelector.vue";
|
||||
import MilestoneSelector from "@/components/MilestoneSelector.vue";
|
||||
import InlineAssistPanel from "@/components/InlineAssistPanel.vue";
|
||||
|
||||
const route = useRoute();
|
||||
const router = useRouter();
|
||||
@@ -41,8 +42,13 @@ const renderedPreview = computed(() => renderMarkdown(body.value));
|
||||
// AI Assist
|
||||
const assist = useAssist(body);
|
||||
|
||||
const renderedStreaming = computed(() => renderMarkdown(assist.streamingText.value));
|
||||
const renderedProposal = computed(() => renderMarkdown(assist.proposedText.value));
|
||||
const assistLabel = computed(() => {
|
||||
if (assist.isProofreading.value) return "Proofreading document...";
|
||||
const t = assist.target.value;
|
||||
if (!t) return "Generating...";
|
||||
const heading = t.text.split("\n")[0];
|
||||
return `Revising: "${heading.length > 50 ? heading.slice(0, 50) + "..." : heading}"`;
|
||||
});
|
||||
|
||||
// Assist panel toggle (persisted)
|
||||
const ASSIST_KEY = 'fa-assist-open';
|
||||
@@ -80,9 +86,6 @@ function handleInlineAssist() {
|
||||
nextTick(() => instructionRef.value?.focus());
|
||||
}
|
||||
|
||||
// Diff view toggle
|
||||
const showFullProposed = ref(false);
|
||||
|
||||
function handleAssistAccept() {
|
||||
const newBody = assist.accept();
|
||||
if (newBody !== body.value) {
|
||||
@@ -90,7 +93,6 @@ function handleAssistAccept() {
|
||||
markDirty();
|
||||
toast.show("Section updated");
|
||||
}
|
||||
showFullProposed.value = false;
|
||||
}
|
||||
|
||||
function truncateTarget(text: string, max = 60): string {
|
||||
@@ -366,7 +368,21 @@ onUnmounted(() => window.removeEventListener("beforeunload", onBeforeUnload));
|
||||
|
||||
<div class="editor-body">
|
||||
<div class="editor-main">
|
||||
<div v-show="!showPreview">
|
||||
<!-- Inline assist output: streaming preview + review diff -->
|
||||
<InlineAssistPanel
|
||||
v-if="assist.state.value !== 'idle'"
|
||||
:phase="assist.state.value as 'streaming' | 'review'"
|
||||
:label="assistLabel"
|
||||
:streaming-text="assist.streamingText.value"
|
||||
:diff="assist.diff.value"
|
||||
:proposed-text="assist.proposedText.value"
|
||||
@accept="handleAssistAccept"
|
||||
@reject="assist.reject()"
|
||||
@cancel="assist.clearSelection()"
|
||||
/>
|
||||
|
||||
<!-- Editor hidden during review so diff takes full column -->
|
||||
<div v-show="!showPreview && assist.state.value !== 'review'">
|
||||
<TiptapEditor
|
||||
ref="editorRef"
|
||||
:modelValue="body"
|
||||
@@ -377,7 +393,7 @@ onUnmounted(() => window.removeEventListener("beforeunload", onBeforeUnload));
|
||||
</div>
|
||||
|
||||
<div
|
||||
v-show="showPreview"
|
||||
v-show="showPreview && assist.state.value !== 'review'"
|
||||
class="preview-pane prose"
|
||||
v-html="renderedPreview"
|
||||
></div>
|
||||
@@ -442,41 +458,9 @@ onUnmounted(() => window.removeEventListener("beforeunload", onBeforeUnload));
|
||||
<!-- ERROR -->
|
||||
<div v-if="assist.error.value" class="assist-error">{{ assist.error.value }}</div>
|
||||
|
||||
<!-- STREAMING -->
|
||||
<div v-if="assist.state.value === 'streaming'" class="assist-streaming">
|
||||
<div class="assist-streaming-label">
|
||||
{{ assist.isProofreading.value
|
||||
? 'Proofreading document...'
|
||||
: `Revising: "${truncateTarget(assist.target.value?.text ?? '')}"` }}
|
||||
</div>
|
||||
<div class="assist-preview-box prose" v-html="renderedStreaming"></div>
|
||||
<span class="typing-indicator">●●●</span>
|
||||
</div>
|
||||
|
||||
<!-- REVIEW -->
|
||||
<div v-if="assist.state.value === 'review'" class="assist-review">
|
||||
<div class="assist-review-header">
|
||||
<span>{{ assist.isProofreading.value ? 'Document proofread' : 'Proposed changes' }}</span>
|
||||
<button class="btn-toggle-view" @click="showFullProposed = !showFullProposed">
|
||||
{{ showFullProposed ? 'Show diff' : 'Show full text' }}
|
||||
</button>
|
||||
</div>
|
||||
<div v-if="!showFullProposed" class="diff-view">
|
||||
<div
|
||||
v-for="(line, i) in assist.diff.value"
|
||||
:key="i"
|
||||
:class="['diff-line', `diff-${line.type}`]"
|
||||
>
|
||||
<span class="diff-marker">{{ line.type === 'delete' ? '−' : line.type === 'insert' ? '+' : ' ' }}</span>
|
||||
<span class="diff-text">{{ line.text }}</span>
|
||||
</div>
|
||||
<div v-if="!assist.diff.value.length" class="diff-empty">No changes.</div>
|
||||
</div>
|
||||
<div v-else class="assist-preview-box prose" v-html="renderedProposal"></div>
|
||||
<div class="assist-actions">
|
||||
<button class="btn-accept" @click="handleAssistAccept">✓ Accept</button>
|
||||
<button class="btn-reject" @click="() => { assist.reject(); showFullProposed = false; }">✗ Reject</button>
|
||||
</div>
|
||||
<!-- Hint when active (output shown inline in editor area) -->
|
||||
<div v-if="assist.state.value !== 'idle'" class="assist-active-hint">
|
||||
{{ assist.state.value === 'streaming' ? 'Generating in editor…' : 'Review diff in editor ↑' }}
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user