Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
74 changes: 74 additions & 0 deletions backend/app/api/graph.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
from ..services.text_processor import TextProcessor
from ..utils.file_parser import FileParser
from ..utils.logger import get_logger
from ..utils.llm_client import LLMClient
from ..models.task import TaskManager, TaskStatus
from ..models.project import ProjectManager, ProjectStatus

Expand Down Expand Up @@ -595,3 +596,76 @@ def delete_graph(graph_id: str):
"error": str(e),
"traceback": traceback.format_exc()
}), 500


# ============== Simple Mode: Auto-generate Seed + Prompt ==============

@graph_bp.route('/simple-generate', methods=['POST'])
def simple_generate():
"""
Given a plain-language description, use the LLM to produce:
- seed_content: a structured context document (the "reality seed" .txt)
- prompt: a structured simulation prompt ready for MiroFish
Body JSON: { "description": "...user's plain-language goal..." }
"""
try:
import datetime
body = request.get_json(force=True, silent=True) or {}
description = (body.get('description') or '').strip()
if not description:
return jsonify({"success": False, "error": "Missing 'description' in request body"}), 400

llm = LLMClient()

system_prompt = """You are an expert simulation designer for MiroFish, an offline multi-agent prediction engine.
Given a plain-language goal from a user, produce TWO outputs as a JSON object with exactly two keys:

1. "seed_content": A structured context document (plain text) that serves as the "reality seed" for the simulation.
Include:
- Date header
- Relevant factual context organized under clear headings
- Key factors (market, geopolitical, social, etc.)
- A concise "Goal:" section at the end

2. "prompt": A concise, structured simulation prompt for an AI agent.
Include:
- Role assignment ("Act as ...")
- The date
- Reference to the provided context
- "Consider:" bullet points
- "Output:" numbered deliverables
- Instruction to be decisive and specific

Return ONLY valid JSON with keys "seed_content" and "prompt". No markdown, no extra explanation."""

today = datetime.date.today().strftime('%B %d, %Y')
user_message = f"Today's date: {today}\n\nUser goal: {description}"

result = llm.chat_json(
messages=[
{"role": "system", "content": system_prompt},
{"role": "user", "content": user_message}
],
temperature=0.5,
max_tokens=4096
)

seed_content = result.get('seed_content', '')
prompt_text = result.get('prompt', '')

if not seed_content or not prompt_text:
return jsonify({"success": False, "error": "LLM returned incomplete output", "raw": result}), 500

return jsonify({
"success": True,
"seed_content": seed_content,
"prompt": prompt_text
})

except Exception as e:
logger.error(f"simple_generate error: {e}")
return jsonify({
"success": False,
"error": str(e),
"traceback": traceback.format_exc()
}), 500
13 changes: 13 additions & 0 deletions frontend/src/api/graph.js
Original file line number Diff line number Diff line change
Expand Up @@ -68,3 +68,16 @@ export function getProject(projectId) {
method: 'get'
})
}

/**
* Simple mode: generate seed document + simulation prompt from a plain-language description
* @param {String} description - User's plain-language prediction goal
* @returns {Promise<{ seed_content: string, prompt: string }>}
*/
export function generateSeedAndPrompt(description) {
return service({
url: '/api/graph/simple-generate',
method: 'post',
data: { description }
})
}
228 changes: 188 additions & 40 deletions frontend/src/views/Home.vue
Original file line number Diff line number Diff line change
Expand Up @@ -86,54 +86,131 @@

<!-- Right Column: Interactive Console -->
<div class="right-panel" :style="s.rightPanel">

<!-- Simple / Advanced Toggle -->
<div :style="s.modeToggleRow">
<button :style="simpleMode ? s.modeTabActive : s.modeTab" @click="switchMode(true)">Simple</button>
<button :style="!simpleMode ? s.modeTabActive : s.modeTab" @click="switchMode(false)">Advanced</button>
<span :style="s.modeHint">{{ simpleMode ? 'Describe your goal β€” we generate the seed &amp; prompt for you' : 'Upload your own seed document and write a custom prompt' }}</span>
</div>

<div class="console-box" :style="s.consoleBox">
<div :style="s.consoleSection">
<div class="console-header" :style="s.consoleHeader">
<span>01 / Reality Seeds</span>
<span>Supported: PDF, MD, TXT</span>

<!-- ───────── SIMPLE MODE ───────── -->
<template v-if="simpleMode">
<div :style="s.consoleSection">
<div class="console-header" :style="s.consoleHeader">
<span>>_ Simple Goal</span>
<span>AI will generate seed + prompt</span>
</div>
<div :style="s.inputWrapper">
<textarea
v-model="simpleDescription"
:style="s.codeInput"
placeholder="// Describe what you want to predict in plain language.&#10;// Example: I want to predict oil prices over the next 7 days and get a buy/sell recommendation for Canadian oil stocks on Questrade."
rows="6"
:disabled="isGenerating || loading"
></textarea>
</div>
</div>

<!-- Error -->
<div v-if="generateError" :style="s.errorBox">{{ generateError }}</div>

<!-- Generate button -->
<div v-if="!generatedSeed" :style="s.btnSection">
<button :style="s.generateBtn" @click="generateSimple" :disabled="!simpleDescription.trim() || isGenerating">
<span v-if="!isGenerating">Generate Seed &amp; Prompt</span>
<span v-else>Generating...</span>
<span>⟳</span>
</button>
</div>
<div
:style="s.uploadZone"
@dragover.prevent="handleDragOver"
@dragleave.prevent="handleDragLeave"
@drop.prevent="handleDrop"
@click="triggerFileInput"
>
<input ref="fileInput" type="file" multiple accept=".pdf,.md,.txt" @change="handleFileSelect" style="display: none" :disabled="loading" />
<div v-if="files.length === 0" :style="s.uploadPlaceholder">
<div :style="s.uploadIcon">↑</div>
<div :style="s.uploadTitle">Drag & drop files here</div>
<div :style="s.uploadHint">or click to browse</div>

<!-- Generated results (editable) -->
<template v-if="generatedSeed">
<div :style="s.consoleDivider"><span :style="s.consoleDividerText">Generated Context (Seed)</span></div>
<div :style="s.consoleSection">
<div class="console-header" :style="s.consoleHeader">
<span>01 / Reality Seed</span>
<span style="color:#FF4500;cursor:pointer;" @click="resetSimple">βœ• Reset</span>
</div>
<div :style="s.inputWrapper">
<textarea v-model="generatedSeed" :style="{ ...s.codeInput, minHeight: '180px' }" rows="8" :disabled="loading"></textarea>
</div>
</div>

<div :style="s.consoleDivider"><span :style="s.consoleDividerText">Generated Prompt</span></div>
<div :style="s.consoleSection">
<div class="console-header" :style="s.consoleHeader">
<span>>_ 02 / Simulation Prompt</span>
</div>
<div :style="s.inputWrapper">
<textarea v-model="generatedPrompt" :style="{ ...s.codeInput, minHeight: '180px' }" rows="8" :disabled="loading"></textarea>
<div :style="s.modelBadge">Engine: Ollama + Neo4j (local)</div>
</div>
</div>

<div :style="s.btnSection">
<button :style="s.startEngineBtn" @click="startSimulation" :disabled="!canSubmitSimple || loading">
<span v-if="!loading">Start Engine</span>
<span v-else>Initializing...</span>
<span>β†’</span>
</button>
</div>
<div v-else :style="s.fileList">
<div v-for="(file, index) in files" :key="index" :style="s.fileItem">
<span>πŸ“„</span>
<span :style="s.fileName">{{ file.name }}</span>
<button @click.stop="removeFile(index)" :style="s.removeBtn">Γ—</button>
</template>
</template>

<!-- ───────── ADVANCED MODE ───────── -->
<template v-else>
<div :style="s.consoleSection">
<div class="console-header" :style="s.consoleHeader">
<span>01 / Reality Seeds</span>
<span>Supported: PDF, MD, TXT</span>
</div>
<div
:style="s.uploadZone"
@dragover.prevent="handleDragOver"
@dragleave.prevent="handleDragLeave"
@drop.prevent="handleDrop"
@click="triggerFileInput"
>
<input ref="fileInput" type="file" multiple accept=".pdf,.md,.txt" @change="handleFileSelect" style="display: none" :disabled="loading" />
<div v-if="files.length === 0" :style="s.uploadPlaceholder">
<div :style="s.uploadIcon">↑</div>
<div :style="s.uploadTitle">Drag &amp; drop files here</div>
<div :style="s.uploadHint">or click to browse</div>
</div>
<div v-else :style="s.fileList">
<div v-for="(file, index) in files" :key="index" :style="s.fileItem">
<span>πŸ“„</span>
<span :style="s.fileName">{{ file.name }}</span>
<button @click.stop="removeFile(index)" :style="s.removeBtn">Γ—</button>
</div>
</div>
</div>
</div>
</div>

<div :style="s.consoleDivider"><span :style="s.consoleDividerText">Parameters</span></div>
<div :style="s.consoleDivider"><span :style="s.consoleDividerText">Parameters</span></div>

<div :style="s.consoleSection">
<div class="console-header" :style="s.consoleHeader">
<span>>_ 02 / Simulation Prompt</span>
<div :style="s.consoleSection">
<div class="console-header" :style="s.consoleHeader">
<span>>_ 02 / Simulation Prompt</span>
</div>
<div :style="s.inputWrapper">
<textarea v-model="formData.simulationRequirement" :style="s.codeInput" placeholder="// Describe your simulation or prediction goal in natural language" rows="6" :disabled="loading"></textarea>
<div :style="s.modelBadge">Engine: Ollama + Neo4j (local)</div>
</div>
</div>
<div :style="s.inputWrapper">
<textarea v-model="formData.simulationRequirement" :style="s.codeInput" placeholder="// Describe your simulation or prediction goal in natural language" rows="6" :disabled="loading"></textarea>
<div :style="s.modelBadge">Engine: Ollama + Neo4j (local)</div>

<div :style="s.btnSection">
<button :style="s.startEngineBtn" @click="startSimulation" :disabled="!canSubmit || loading">
<span v-if="!loading">Start Engine</span>
<span v-else>Initializing...</span>
<span>β†’</span>
</button>
</div>
</div>
</template>

<div :style="s.btnSection">
<button :style="s.startEngineBtn" @click="startSimulation" :disabled="!canSubmit || loading">
<span v-if="!loading">Start Engine</span>
<span v-else>Initializing...</span>
<span>β†’</span>
</button>
</div>
</div>
</div>
</section>
Expand All @@ -147,6 +224,7 @@
import { ref, computed, reactive } from 'vue'
import { useRouter } from 'vue-router'
import HistoryDatabase from '../components/HistoryDatabase.vue'
import { generateSeedAndPrompt } from '../api/graph'

const mono = 'JetBrains Mono, monospace'
const sans = 'Space Grotesk, Noto Sans SC, system-ui, sans-serif'
Expand Down Expand Up @@ -196,6 +274,12 @@ const s = reactive({
stepTitle: { fontWeight: '520', fontSize: '1rem', marginBottom: '4px' },
stepDesc: { fontSize: '0.85rem', color: '#666' },
rightPanel: { flex: '1.2', display: 'flex', flexDirection: 'column' },
// Mode toggle
modeToggleRow: { display: 'flex', alignItems: 'center', gap: '0', marginBottom: '12px', flexWrap: 'wrap', rowGap: '8px' },
modeTab: { fontFamily: mono, fontSize: '0.8rem', fontWeight: '700', letterSpacing: '1px', padding: '8px 22px', border: '1px solid #CCC', background: '#fff', color: '#888', cursor: 'pointer' },
modeTabActive: { fontFamily: mono, fontSize: '0.8rem', fontWeight: '700', letterSpacing: '1px', padding: '8px 22px', border: '1px solid #000', background: '#000', color: '#fff', cursor: 'pointer' },
modeHint: { fontFamily: mono, fontSize: '0.72rem', color: '#AAA', marginLeft: '16px' },
// Console
consoleBox: { border: '1px solid #CCC', padding: '8px' },
consoleSection: { padding: '20px' },
consoleHeader: { display: 'flex', justifyContent: 'space-between', marginBottom: '15px', fontFamily: mono, fontSize: '0.75rem', color: '#666' },
Expand All @@ -211,10 +295,12 @@ const s = reactive({
consoleDivider: { display: 'flex', alignItems: 'center', margin: '10px 0', borderTop: '1px solid #EEE' },
consoleDividerText: { padding: '0 15px', fontFamily: mono, fontSize: '0.7rem', color: '#BBB', letterSpacing: '1px' },
inputWrapper: { position: 'relative', border: '1px solid #DDD', background: '#FAFAFA' },
codeInput: { width: '100%', border: 'none', background: 'transparent', padding: '20px', fontFamily: mono, fontSize: '0.9rem', lineHeight: '1.6', resize: 'vertical', outline: 'none', minHeight: '150px' },
codeInput: { width: '100%', border: 'none', background: 'transparent', padding: '20px', fontFamily: mono, fontSize: '0.9rem', lineHeight: '1.6', resize: 'vertical', outline: 'none', minHeight: '150px', boxSizing: 'border-box' },
modelBadge: { position: 'absolute', bottom: '10px', right: '15px', fontFamily: mono, fontSize: '0.7rem', color: '#AAA' },
btnSection: { padding: '0 20px 20px' },
startEngineBtn: { width: '100%', background: '#000', color: '#fff', border: 'none', padding: '20px', fontFamily: mono, fontWeight: '700', fontSize: '1.1rem', display: 'flex', justifyContent: 'space-between', alignItems: 'center', cursor: 'pointer', letterSpacing: '1px' },
generateBtn: { width: '100%', background: '#FF4500', color: '#fff', border: 'none', padding: '18px 20px', fontFamily: mono, fontWeight: '700', fontSize: '1rem', display: 'flex', justifyContent: 'space-between', alignItems: 'center', cursor: 'pointer', letterSpacing: '1px' },
errorBox: { margin: '0 20px 12px', padding: '12px 16px', border: '1px solid #FF4500', background: '#FFF5F2', color: '#FF4500', fontFamily: mono, fontSize: '0.8rem' },
})

const steps = [
Expand All @@ -227,17 +313,38 @@ const steps = [

const router = useRouter()

// ── Advanced mode state ──
const formData = ref({ simulationRequirement: '' })
const files = ref([])
const loading = ref(false)
const error = ref('')
const isDragOver = ref(false)
const fileInput = ref(null)

// ── Simple mode state ──
const simpleMode = ref(true)
const simpleDescription = ref('')
const isGenerating = ref(false)
const generatedSeed = ref('')
const generatedPrompt = ref('')
const generateError = ref('')

// ── Mode switching ──
const switchMode = (toSimple) => {
simpleMode.value = toSimple
generateError.value = ''
}

// ── Computed ──
const canSubmit = computed(() => {
return formData.value.simulationRequirement.trim() !== '' && files.value.length > 0
})

const canSubmitSimple = computed(() => {
return generatedSeed.value.trim() !== '' && generatedPrompt.value.trim() !== ''
})

// ── Advanced mode handlers ──
const triggerFileInput = () => { if (!loading.value) fileInput.value?.click() }
const handleFileSelect = (event) => { addFiles(Array.from(event.target.files)) }
const handleDragOver = (e) => { isDragOver.value = true }
Expand All @@ -254,10 +361,51 @@ const removeFile = (index) => { files.value.splice(index, 1) }

const scrollToBottom = () => { window.scrollTo({ top: document.body.scrollHeight, behavior: 'smooth' }) }

// ── Simple mode: generate seed + prompt via LLM ──
const generateSimple = async () => {
if (!simpleDescription.value.trim() || isGenerating.value) return
isGenerating.value = true
generateError.value = ''
generatedSeed.value = ''
generatedPrompt.value = ''
try {
const res = await generateSeedAndPrompt(simpleDescription.value.trim())
// axios interceptor already unwraps response.data, so res IS the payload
if (res && res.seed_content && res.prompt) {
generatedSeed.value = res.seed_content
generatedPrompt.value = res.prompt
} else {
generateError.value = res?.error || 'Generation failed β€” please try again.'
}
} catch (e) {
generateError.value = e?.response?.data?.error || e.message || 'Network error during generation.'
} finally {
isGenerating.value = false
}
}

const resetSimple = () => {
generatedSeed.value = ''
generatedPrompt.value = ''
generateError.value = ''
}

// ── Start simulation ──
const startSimulation = () => {
if (!canSubmit.value || loading.value) return
if (loading.value) return
import('../store/pendingUpload.js').then(({ setPendingUpload }) => {
setPendingUpload(files.value, formData.value.simulationRequirement)
if (simpleMode.value) {
// Create an in-memory .txt File from the generated seed content
const seedFile = new File(
[generatedSeed.value],
'seed.txt',
{ type: 'text/plain' }
)
setPendingUpload([seedFile], generatedPrompt.value)
} else {
if (!canSubmit.value) return
setPendingUpload(files.value, formData.value.simulationRequirement)
}
router.push({ name: 'Process', params: { projectId: 'new' } })
})
}
Expand Down