Implement optional single-image intake, Signal ingestion, and multimodal vision analysis

- Add image intake service with format validation (JPEG, PNG, WebP) and EXIF/GPS stripping
- Enforce strict single-image rule across Web and Signal attachment channels
- Implement token-optimized vision downscaling and JPEG compression
- Add IMAGE_CONTEXT pipeline stage with OmniRoute vision routing and resilient failover
- Seed and manage versioned idea-image-interpreter prompt in catalog
- Update Web UI with responsive image picker, preview chip, and Visual Context tab
- Add comprehensive automated test suite in test_image_intake.py
- Update README and Labyricorn devlog
This commit is contained in:
2026-08-23 01:40:22 -07:00
parent 94ff1e4408
commit 41e08611c9
30 changed files with 3784 additions and 278 deletions
+342 -37
View File
@@ -65,7 +65,16 @@
Sign in to Claim
</a>
{% endif %}
<button onclick="trashIdea('{{ idea.id }}')" class="btn btn-secondary btn-sm" style="margin-top:0.4rem; color:#f87171;" title="Move idea to trash">
{% if idea.enrichment_level < 4 %}
<button onclick="reprocessIdea('{{ idea.id }}')" class="btn btn-warning btn-sm" style="margin-top:0.3rem;" title="Generate missing research documents">
⚡ Generate Research
</button>
{% else %}
<button onclick="reprocessIdea('{{ idea.id }}')" class="btn btn-secondary btn-sm" style="margin-top:0.3rem;" title="Re-run AI research and critique">
🔄 Re-run Research
</button>
{% endif %}
<button onclick="trashIdea('{{ idea.id }}')" class="btn btn-secondary btn-sm" style="margin-top:0.2rem; color:#f87171;" title="Move idea to trash">
🗑️ Move to Trash
</button>
{% elif idea.lifecycle_state == 'CLAIMED' %}
@@ -80,7 +89,10 @@
<button onclick="releaseIdea('{{ idea.id }}')" class="btn btn-secondary btn-sm">
Release Claim
</button>
<button onclick="trashIdea('{{ idea.id }}')" class="btn btn-secondary btn-sm" style="margin-top:0.4rem; color:#f87171;" title="Move idea to trash">
<button onclick="reprocessIdea('{{ idea.id }}')" class="btn btn-secondary btn-sm" style="margin-top:0.3rem;" title="Re-run research pipeline">
🔄 Re-run Research
</button>
<button onclick="trashIdea('{{ idea.id }}')" class="btn btn-secondary btn-sm" style="margin-top:0.3rem; color:#f87171;" title="Move idea to trash">
🗑️ Move to Trash
</button>
{% endif %}
@@ -93,12 +105,25 @@
<button onclick="releaseIdea('{{ idea.id }}')" class="btn btn-secondary btn-sm" style="margin-top:0.4rem;">
Release Claim
</button>
<button onclick="trashIdea('{{ idea.id }}')" class="btn btn-secondary btn-sm" style="margin-top:0.4rem; color:#f87171;" title="Move idea to trash">
<button onclick="reprocessIdea('{{ idea.id }}')" class="btn btn-secondary btn-sm" style="margin-top:0.3rem;" title="Re-run research pipeline">
🔄 Re-run Research
</button>
<button onclick="trashIdea('{{ idea.id }}')" class="btn btn-secondary btn-sm" style="margin-top:0.3rem; color:#f87171;" title="Move idea to trash">
🗑️ Move to Trash
</button>
{% endif %}
{% elif idea.lifecycle_state == 'DUPLICATE' %}
<button onclick="reprocessIdea('{{ idea.id }}')" class="btn btn-warning btn-sm" style="width:100%; font-weight:700;">
⚡ Force Run Research
</button>
<button onclick="trashIdea('{{ idea.id }}')" class="btn btn-secondary btn-sm" style="margin-top:0.3rem; color:#f87171;" title="Move idea to trash">
🗑️ Move to Trash
</button>
{% else %}
<button onclick="trashIdea('{{ idea.id }}')" class="btn btn-secondary btn-sm" style="color:#f87171;" title="Move idea to trash">
<button onclick="reprocessIdea('{{ idea.id }}')" class="btn btn-primary btn-sm" style="width:100%; font-weight:700;">
⚡ Process Idea Pipeline
</button>
<button onclick="trashIdea('{{ idea.id }}')" class="btn btn-secondary btn-sm" style="margin-top:0.3rem; color:#f87171;" title="Move idea to trash">
🗑️ Move to Trash
</button>
{% endif %}
@@ -120,7 +145,7 @@
<div style="display:flex; align-items:center; gap:0.4rem;">
<span style="font-size:0.8rem; color:var(--text-muted);">Tags:</span>
{% for t in idea.tags %}
<span class="tag-item">#{{ t }}</span>
<span class="badge" style="background:rgba(99,102,241,0.12); color:#a5b4fc;">#{{ t }}</span>
{% endfor %}
</div>
{% endif %}
@@ -131,13 +156,54 @@
</div>
</div>
{% if idea.lifecycle_state == 'DUPLICATE' %}
<div class="glass-card" style="margin-bottom:1.5rem; border-color:rgba(245, 158, 11, 0.4); background:rgba(245, 158, 11, 0.08); padding:1.25rem;">
<div style="display:flex; justify-content:space-between; align-items:center; flex-wrap:wrap; gap:1rem;">
<div>
<div style="font-weight:700; color:#fbbf24; display:flex; align-items:center; gap:0.5rem; font-size:1.05rem;">
<span>⚠️ Potential Duplicate Submission Detected</span>
</div>
<p style="margin:0.4rem 0 0 0; font-size:0.9rem; color:var(--text-secondary); max-width:800px;">
Automatic pipeline flagged this idea as closely matching existing catalog submissions and paused downstream deep research generation to conserve compute. You can override this and force full research generation at any time.
</p>
</div>
<button onclick="reprocessIdea('{{ idea.id }}')" class="btn btn-primary btn-sm" style="background:linear-gradient(135deg, #f59e0b 0%, #d97706 100%); color:#000; font-weight:700;">
⚡ Force Run Research Pipeline
</button>
</div>
</div>
{% endif %}
<!-- Original Submission & URL Safety Box -->
<div style="display:grid; grid-template-columns: 2fr 1fr; gap:1.5rem; margin-bottom:2rem;">
<div class="glass-card" style="padding:1.5rem;">
<h3 style="font-size:1.1rem; margin-bottom:0.75rem; color:var(--text-secondary); display:flex; align-items:center; gap:0.5rem;">
<span>📝 Immutable Original Submission</span>
</h3>
<div style="background:rgba(0,0,0,0.3); border:1px solid var(--border-glass); border-radius:var(--radius-md); padding:1rem; font-family:var(--font-sans); line-height:1.6; white-space:pre-wrap;">{{ idea.original_text }}</div>
<div class="markdown-view-container" data-markdown-view>
<div class="markdown-view-header">
<h3 style="font-size:1.1rem; margin:0; color:var(--text-secondary); display:flex; align-items:center; gap:0.5rem;">
<span>📝 Immutable Original Submission</span>
</h3>
<div class="markdown-view-actions">
<div class="markdown-toggle-pill">
<button type="button" class="btn-toggle-view active" data-view="formatted" title="Formatted View">
<span>👁️ Formatted</span>
</button>
<button type="button" class="btn-toggle-view" data-view="code" title="Code / Raw View">
<span>💻 Code</span>
</button>
</div>
<button type="button" class="btn-copy-markdown" onclick="copyMarkdownFromContainer(this)" title="Copy Text">
<span>📋 Copy</span>
</button>
</div>
</div>
<div class="markdown-view-body" style="background:rgba(0,0,0,0.3); border:1px solid var(--border-glass); border-radius:var(--radius-md); padding:1rem;">
<div class="markdown-formatted markdown-body"></div>
<div class="markdown-code" style="display:none; background:transparent; border:none; padding:0; margin:0;">
<pre style="margin:0; background:transparent; border:none; padding:0; line-height:1.6;"><code>{{ idea.original_text }}</code></pre>
</div>
<textarea class="raw-markdown-source" style="display:none;">{{ idea.original_text }}</textarea>
</div>
</div>
</div>
<div class="glass-card" style="padding:1.5rem;">
@@ -164,9 +230,43 @@
</div>
</div>
<!-- Reference Image Card (if attached) -->
{% if idea.submission_image and idea.submission_image.present %}
<div class="glass-card" style="padding:1.5rem; margin-bottom:2rem;">
<div style="display:flex; justify-content:space-between; align-items:center; margin-bottom:1rem; flex-wrap:wrap; gap:0.5rem;">
<h3 style="font-size:1.1rem; margin:0; color:var(--text-secondary); display:flex; align-items:center; gap:0.5rem;">
<span>🖼️ Attached Reference Image</span>
<span style="font-family:var(--font-mono); font-size:0.85rem; color:var(--accent-primary);">{{ idea.submission_image.artifact_id }}</span>
</h3>
<div style="display:flex; gap:0.5rem; flex-wrap:wrap; font-size:0.78rem;">
<span class="badge" style="background:rgba(255,255,255,0.06);">{{ idea.submission_image.mime_type }}</span>
<span class="badge" style="background:rgba(255,255,255,0.06);">{{ idea.submission_image.width }}x{{ idea.submission_image.height }}</span>
<span class="badge" style="background:rgba(255,255,255,0.06);">{{ (idea.submission_image.size_bytes / 1024)|round(1) }} KB</span>
<span class="badge" style="background:rgba(99,102,241,0.12); color:#a5b4fc;">Source: {{ idea.submission_image.source|capitalize }}</span>
</div>
</div>
<div style="display:flex; flex-direction:column; align-items:center; background:rgba(0,0,0,0.3); border:1px solid var(--border-glass); border-radius:var(--radius-md); padding:1rem;">
<a href="/api/ideas/{{ idea.id }}/image" target="_blank" title="Click to open full-resolution image in new tab">
<img
src="/api/ideas/{{ idea.id }}/image"
alt="Reference visual artifact for {{ idea.id }}"
style="max-width:100%; max-height:480px; width:auto; height:auto; object-fit:contain; border-radius:var(--radius-sm); display:block; border:1px solid var(--border-glass);"
/>
</a>
<div style="margin-top:0.75rem; font-family:var(--font-mono); font-size:0.75rem; color:var(--text-muted); width:100%; display:flex; justify-content:space-between; flex-wrap:wrap; gap:0.5rem;">
<span>File: {{ idea.submission_image.original_filename }}</span>
<span>SHA-256: <code>{{ idea.submission_image.sha256 }}</code></span>
</div>
</div>
</div>
{% endif %}
<!-- Tabs Section -->
<div class="tab-container glass-card" style="padding:1.75rem;">
<div class="tab-nav">
{% if idea.submission_image and idea.submission_image.present %}
<button class="tab-btn" data-tab="tab-image-context">🖼️ Visual & Image Context</button>
{% endif %}
<button class="tab-btn active" data-tab="tab-prior-art">🔍 Prior Art & Alternatives</button>
<button class="tab-btn" data-tab="tab-research">📚 Deep Research Synthesis</button>
<button class="tab-btn" data-tab="tab-feasibility">⚙️ Feasibility & Critique</button>
@@ -175,40 +275,206 @@
<button class="tab-btn" data-tab="tab-artifacts">📦 Dossier & Gitea Project</button>
</div>
<!-- Tab 0: Image Context (if present) -->
{% if idea.submission_image and idea.submission_image.present %}
<div id="tab-image-context" class="tab-panel">
{% set image_ctx_run = idea.provenance | selectattr("stage", "equalto", "IMAGE_CONTEXT") | list %}
{% if image_ctx_run and image_ctx_run|length > 0 and image_ctx_run[0].output_data.content %}
<div class="markdown-view-container" data-markdown-view>
<div class="markdown-view-header">
<div class="markdown-view-meta">
<span class="badge" style="background:rgba(99,102,241,0.15); color:#a5b4fc;">Stage: IMAGE_CONTEXT</span>
<span class="badge" style="background:rgba(34,197,94,0.15); color:#4ade80;">AI Visual Interpretation</span>
{% if image_ctx_run[0].resolved_model %}
<span class="badge" style="background:rgba(255,255,255,0.06); font-size:0.75rem;">🤖 {{ image_ctx_run[0].resolved_model }}</span>
{% endif %}
</div>
<div class="markdown-view-actions">
<div class="markdown-toggle-pill">
<button type="button" class="btn-toggle-view active" data-view="formatted" title="Formatted View">
<span>👁️ Formatted</span>
</button>
<button type="button" class="btn-toggle-view" data-view="code" title="Code / Raw View">
<span>💻 Code</span>
</button>
</div>
<button type="button" class="btn-copy-markdown" onclick="copyMarkdownFromContainer(this)" title="Copy Markdown">
<span>📋 Copy</span>
</button>
</div>
</div>
<div class="markdown-view-body">
<div class="markdown-formatted markdown-body"></div>
<div class="markdown-code" style="display:none;">
<pre><code>{{ image_ctx_run[0].output_data.content }}</code></pre>
</div>
<textarea class="raw-markdown-source" style="display:none;">{{ image_ctx_run[0].output_data.content }}</textarea>
</div>
</div>
{% elif image_ctx_run and image_ctx_run|length > 0 and image_ctx_run[0].status == 'FAILED' %}
<div class="glass-card" style="padding:2.5rem 2rem; text-align:center; background:rgba(239,68,68,0.04); border:1px dashed rgba(239,68,68,0.3); border-radius:var(--radius-md);">
<div style="font-size:2.2rem; margin-bottom:0.75rem;">⚠️</div>
<h4 style="color:#f87171; margin-bottom:0.5rem;">Vision Processing Notice</h4>
<p style="color:var(--text-secondary); max-width:550px; margin:0 auto; font-size:0.9rem;">
The reference image was safely preserved, but automated vision analysis was unavailable or encountered an error:
<code>{{ image_ctx_run[0].error_message or "Model refused or failed image processing." }}</code>.
The idea continues with text-only research synthesis.
</p>
</div>
{% else %}
<div class="glass-card" style="padding:2.5rem 2rem; text-align:center; background:rgba(255,255,255,0.02); border:1px dashed var(--border-glass); border-radius:var(--radius-md);">
<div style="font-size:2.2rem; margin-bottom:0.75rem;">🖼️</div>
<h4 style="color:var(--text-secondary); margin-bottom:0.5rem;">Visual Context Queued</h4>
<p style="color:var(--text-muted); max-width:450px; margin:0 auto; font-size:0.9rem;">
Image interpretation is queued or processing. Visual context will appear here once synthesized.
</p>
</div>
{% endif %}
</div>
{% endif %}
<!-- Tab 1: Prior Art -->
<div id="tab-prior-art" class="tab-panel active">
<div class="markdown-body">
{% set prior_art_run = idea.provenance | selectattr("stage", "equalto", "PRIOR_ART") | list %}
{% if prior_art_run and prior_art_run|length > 0 and prior_art_run[0].output_data.content %}
<div style="white-space:pre-wrap;">{{ prior_art_run[0].output_data.content }}</div>
{% else %}
<p style="color:var(--text-muted);">Prior art discovery is processing or pending.</p>
{% endif %}
</div>
{% set prior_art_run = idea.provenance | selectattr("stage", "equalto", "PRIOR_ART") | list %}
{% if prior_art_run and prior_art_run|length > 0 and prior_art_run[0].output_data.content %}
<div class="markdown-view-container" data-markdown-view>
<div class="markdown-view-header">
<div class="markdown-view-meta">
<span class="badge" style="background:rgba(99,102,241,0.15); color:#a5b4fc;">Stage: PRIOR_ART</span>
{% if prior_art_run[0].resolved_model %}
<span class="badge" style="background:rgba(255,255,255,0.06); font-size:0.75rem;">🤖 {{ prior_art_run[0].resolved_model }}</span>
{% endif %}
</div>
<div class="markdown-view-actions">
<div class="markdown-toggle-pill">
<button type="button" class="btn-toggle-view active" data-view="formatted" title="Formatted View">
<span>👁️ Formatted</span>
</button>
<button type="button" class="btn-toggle-view" data-view="code" title="Code / Raw View">
<span>💻 Code</span>
</button>
</div>
<button type="button" class="btn-copy-markdown" onclick="copyMarkdownFromContainer(this)" title="Copy Markdown">
<span>📋 Copy</span>
</button>
</div>
</div>
<div class="markdown-view-body">
<div class="markdown-formatted markdown-body"></div>
<div class="markdown-code" style="display:none;">
<pre><code>{{ prior_art_run[0].output_data.content }}</code></pre>
</div>
<textarea class="raw-markdown-source" style="display:none;">{{ prior_art_run[0].output_data.content }}</textarea>
</div>
</div>
{% else %}
<div class="glass-card" style="padding:2.5rem 2rem; text-align:center; background:rgba(255,255,255,0.02); border:1px dashed var(--border-glass); border-radius:var(--radius-md);">
<div style="font-size:2.2rem; margin-bottom:0.75rem;">🔍</div>
<h4 style="font-size:1.15rem; margin-bottom:0.4rem; color:var(--text-primary);">Prior Art & Competitive Discovery Not Generated</h4>
<p style="color:var(--text-secondary); max-width:560px; margin:0 auto 1.25rem auto; font-size:0.9rem;">
Prior art synthesis was not performed yet or was paused during intake. You can trigger the automated research pipeline to perform market and repository discovery now.
</p>
<button onclick="reprocessIdea('{{ idea.id }}')" class="btn btn-primary" style="display:inline-flex; align-items:center; gap:0.5rem; font-weight:600; padding:0.6rem 1.25rem;">
<span>⚡ Generate Prior Art & Research Documents</span>
</button>
</div>
{% endif %}
</div>
<!-- Tab 2: Research Synthesis -->
<div id="tab-research" class="tab-panel">
<div class="markdown-body">
{% set research_run = idea.provenance | selectattr("stage", "equalto", "RESEARCH") | list %}
{% if research_run and research_run|length > 0 and research_run[0].output_data.content %}
<div style="white-space:pre-wrap;">{{ research_run[0].output_data.content }}</div>
{% else %}
<p style="color:var(--text-muted);">Deep research synthesis is processing or pending.</p>
{% endif %}
</div>
{% set research_run = idea.provenance | selectattr("stage", "equalto", "RESEARCH") | list %}
{% if research_run and research_run|length > 0 and research_run[0].output_data.content %}
<div class="markdown-view-container" data-markdown-view>
<div class="markdown-view-header">
<div class="markdown-view-meta">
<span class="badge" style="background:rgba(99,102,241,0.15); color:#a5b4fc;">Stage: RESEARCH</span>
{% if research_run[0].resolved_model %}
<span class="badge" style="background:rgba(255,255,255,0.06); font-size:0.75rem;">🤖 {{ research_run[0].resolved_model }}</span>
{% endif %}
</div>
<div class="markdown-view-actions">
<div class="markdown-toggle-pill">
<button type="button" class="btn-toggle-view active" data-view="formatted" title="Formatted View">
<span>👁️ Formatted</span>
</button>
<button type="button" class="btn-toggle-view" data-view="code" title="Code / Raw View">
<span>💻 Code</span>
</button>
</div>
<button type="button" class="btn-copy-markdown" onclick="copyMarkdownFromContainer(this)" title="Copy Markdown">
<span>📋 Copy</span>
</button>
</div>
</div>
<div class="markdown-view-body">
<div class="markdown-formatted markdown-body"></div>
<div class="markdown-code" style="display:none;">
<pre><code>{{ research_run[0].output_data.content }}</code></pre>
</div>
<textarea class="raw-markdown-source" style="display:none;">{{ research_run[0].output_data.content }}</textarea>
</div>
</div>
{% else %}
<div class="glass-card" style="padding:2.5rem 2rem; text-align:center; background:rgba(255,255,255,0.02); border:1px dashed var(--border-glass); border-radius:var(--radius-md);">
<div style="font-size:2.2rem; margin-bottom:0.75rem;">🔬</div>
<h4 style="font-size:1.15rem; margin-bottom:0.4rem; color:var(--text-primary);">Deep Research Synthesis Not Generated</h4>
<p style="color:var(--text-secondary); max-width:560px; margin:0 auto 1.25rem auto; font-size:0.9rem;">
Technical analysis and architectural synthesis were not generated yet. You can trigger the automated research engine to analyze this submission now.
</p>
<button onclick="reprocessIdea('{{ idea.id }}')" class="btn btn-primary" style="display:inline-flex; align-items:center; gap:0.5rem; font-weight:600; padding:0.6rem 1.25rem;">
<span>⚡ Generate Deep Research Synthesis</span>
</button>
</div>
{% endif %}
</div>
<!-- Tab 3: Feasibility & Critique -->
<div id="tab-feasibility" class="tab-panel">
<div class="markdown-body">
{% set feas_run = idea.provenance | selectattr("stage", "equalto", "FEASIBILITY") | list %}
{% if feas_run and feas_run|length > 0 and feas_run[0].output_data.content %}
<div style="white-space:pre-wrap;">{{ feas_run[0].output_data.content }}</div>
{% else %}
<p style="color:var(--text-muted);">Feasibility and risk critique is processing or pending.</p>
{% endif %}
</div>
{% set feas_run = idea.provenance | selectattr("stage", "equalto", "FEASIBILITY") | list %}
{% if feas_run and feas_run|length > 0 and feas_run[0].output_data.content %}
<div class="markdown-view-container" data-markdown-view>
<div class="markdown-view-header">
<div class="markdown-view-meta">
<span class="badge" style="background:rgba(99,102,241,0.15); color:#a5b4fc;">Stage: FEASIBILITY</span>
{% if feas_run[0].resolved_model %}
<span class="badge" style="background:rgba(255,255,255,0.06); font-size:0.75rem;">🤖 {{ feas_run[0].resolved_model }}</span>
{% endif %}
</div>
<div class="markdown-view-actions">
<div class="markdown-toggle-pill">
<button type="button" class="btn-toggle-view active" data-view="formatted" title="Formatted View">
<span>👁️ Formatted</span>
</button>
<button type="button" class="btn-toggle-view" data-view="code" title="Code / Raw View">
<span>💻 Code</span>
</button>
</div>
<button type="button" class="btn-copy-markdown" onclick="copyMarkdownFromContainer(this)" title="Copy Markdown">
<span>📋 Copy</span>
</button>
</div>
</div>
<div class="markdown-view-body">
<div class="markdown-formatted markdown-body"></div>
<div class="markdown-code" style="display:none;">
<pre><code>{{ feas_run[0].output_data.content }}</code></pre>
</div>
<textarea class="raw-markdown-source" style="display:none;">{{ feas_run[0].output_data.content }}</textarea>
</div>
</div>
{% else %}
<div class="glass-card" style="padding:2.5rem 2rem; text-align:center; background:rgba(255,255,255,0.02); border:1px dashed var(--border-glass); border-radius:var(--radius-md);">
<div style="font-size:2.2rem; margin-bottom:0.75rem;">⚖️</div>
<h4 style="font-size:1.15rem; margin-bottom:0.4rem; color:var(--text-primary);">Feasibility & Risk Critique Not Generated</h4>
<p style="color:var(--text-secondary); max-width:560px; margin:0 auto 1.25rem auto; font-size:0.9rem;">
Architectural risk assessment and feasibility scoring were not generated yet. You can trigger the critique engine now.
</p>
<button onclick="reprocessIdea('{{ idea.id }}')" class="btn btn-primary" style="display:inline-flex; align-items:center; gap:0.5rem; font-weight:600; padding:0.6rem 1.25rem;">
<span>⚡ Generate Feasibility & Risk Critique</span>
</button>
</div>
{% endif %}
</div>
<!-- Tab 4: Work Tracks -->
@@ -216,7 +482,7 @@
<div style="display:flex; justify-content:space-between; align-items:center; margin-bottom:1.5rem; flex-wrap:wrap; gap:1rem;">
<div>
<h3 style="font-size:1.2rem;">Independent Work Tracks</h3>
<p style="color:var(--text-secondary); font-size:0.9rem;">Assign multiple work types (Article, Blog Entry, Coding Project) to produce distinct outputs.</p>
<p style="color:var(--text-secondary); font-size:0.9rem;">Assign multiple work types (Article, Blog Entry, Coding Project, YouTube Video) to produce distinct outputs.</p>
</div>
{% if user and (user.username == idea.claimed_by or user.role.value == 'ADMIN') %}
@@ -225,6 +491,7 @@
<option value="ARTICLE">Article / Essay</option>
<option value="BLOG_ENTRY">Blog Entry</option>
<option value="CODING_PROJECT">Coding Project</option>
<option value="YOUTUBE_VIDEO">YouTube Video</option>
</select>
<input type="text" id="work-track-name" class="form-input" placeholder="Track Name (e.g. Technical Blueprint)" style="width:200px; padding:0.4rem 0.8rem; font-size:0.85rem;">
<select id="work-model-select" class="form-select" style="width:auto; padding:0.4rem 0.8rem; font-size:0.85rem;" title="Select AI Model">
@@ -294,8 +561,8 @@
</div>
<div style="display:flex; flex-direction:column; gap:0.75rem;">
{% for out in tr.outputs %}
<details {% if out.is_current %}open{% endif %} style="background:rgba(255,255,255,0.03); border:1px solid var(--border-glass); border-radius:var(--radius-sm); padding:0.75rem 1rem;">
<summary style="cursor:pointer; font-weight:600; font-family:var(--font-mono); font-size:0.9rem; color:var(--accent-primary); display:flex; justify-content:space-between; align-items:center; flex-wrap:wrap; gap:0.5rem;">
<details {% if out.is_current %}open{% endif %} style="background:rgba(255,255,255,0.03); border:1px solid var(--border-glass); border-radius:var(--radius-sm); padding:0.75rem 1rem; min-width:0; max-width:100%; overflow:hidden;">
<summary style="cursor:pointer; font-weight:600; font-family:var(--font-mono); font-size:0.9rem; color:var(--accent-primary); display:flex; justify-content:space-between; align-items:center; flex-wrap:wrap; gap:0.5rem; min-width:0;">
<div style="display:flex; align-items:center; flex-wrap:wrap; gap:0.4rem;">
<span>📄 {{ out.name }}</span>
{% if out.is_current %}
@@ -317,7 +584,35 @@
{% endif %}
</div>
</summary>
<div class="markdown-body" style="margin-top:0.75rem; white-space:pre-wrap; border-top:1px solid var(--border-glass); padding-top:0.75rem; font-size:0.92rem; line-height:1.6;">{{ out.content }}</div>
<div style="margin-top:0.75rem; border-top:1px solid var(--border-glass); padding-top:0.75rem; min-width:0; max-width:100%; overflow-x:auto;">
<div class="markdown-view-container" data-markdown-view style="width:100%; min-width:0; max-width:100%;">
<div class="markdown-view-header" style="margin-bottom:0.75rem;">
<div class="markdown-view-meta">
<span style="font-size:0.8rem; color:var(--text-muted); font-family:var(--font-mono);">Path: {{ out.artifact_path }}</span>
</div>
<div class="markdown-view-actions">
<div class="markdown-toggle-pill">
<button type="button" class="btn-toggle-view active" data-view="formatted" title="Formatted View">
<span>👁️ Formatted</span>
</button>
<button type="button" class="btn-toggle-view" data-view="code" title="Code / Raw View">
<span>💻 Code</span>
</button>
</div>
<button type="button" class="btn-copy-markdown" onclick="copyMarkdownFromContainer(this)" title="Copy Markdown">
<span>📋 Copy</span>
</button>
</div>
</div>
<div class="markdown-view-body">
<div class="markdown-formatted markdown-body"></div>
<div class="markdown-code" style="display:none;">
<pre><code>{{ out.content }}</code></pre>
</div>
<textarea class="raw-markdown-source" style="display:none;">{{ out.content }}</textarea>
</div>
</div>
</div>
</details>
{% endfor %}
</div>
@@ -441,6 +736,16 @@
<span>📊 metadata.json</span>
<span style="color:var(--text-muted);">Structured Metadata & State</span>
</div>
{% if idea.submission_image and idea.submission_image.present %}
<div style="padding:0.4rem 0.6rem; background:rgba(99,102,241,0.08); border-radius:var(--radius-sm); display:flex; justify-content:space-between;">
<span>🖼️ {{ idea.submission_image.artifact_id }} ({{ idea.submission_image.mime_type }})</span>
<span style="color:#a5b4fc;">Reference Image Artifact</span>
</div>
<div style="padding:0.4rem 0.6rem; background:rgba(255,255,255,0.03); border-radius:var(--radius-sm); display:flex; justify-content:space-between;">
<span>📄 research/image-context.md</span>
<span style="color:var(--text-muted);">Visual Interpretation Context</span>
</div>
{% endif %}
<div style="padding:0.4rem 0.6rem; background:rgba(255,255,255,0.03); border-radius:var(--radius-sm); display:flex; justify-content:space-between;">
<span>🔍 research/prior-art.md</span>
<span style="color:var(--text-muted);">Competitor Analysis</span>