| import React, { useState, useRef, useCallback } from "react"; |
| import { |
| Upload, |
| FileCode, |
| Plus, |
| Trash2, |
| AlertCircle, |
| CheckCircle2, |
| Settings2, |
| } from "lucide-react"; |
| import { parseYAML, validateYAML, toYAML, PROTOCOLS, DEFAULT_YAML } from "../utils/yamlUtils"; |
|
|
| export default function YAMLForm({ onSpecChange, validationErrors }) { |
| const [yamlText, setYamlText] = useState(DEFAULT_YAML); |
| const [parsed, setParsed] = useState(() => parseYAML(DEFAULT_YAML)); |
| const [mode, setMode] = useState("editor"); |
| const [activeTab, setActiveTab] = useState("spec"); |
| const fileRef = useRef(null); |
|
|
| const handleChange = useCallback( |
| (text) => { |
| setYamlText(text); |
| try { |
| const obj = parseYAML(text); |
| setParsed(obj); |
| onSpecChange(obj); |
| } catch { |
| |
| } |
| }, |
| [onSpecChange] |
| ); |
|
|
| const handleFileUpload = useCallback( |
| (e) => { |
| const file = e.target.files?.[0]; |
| if (!file) return; |
| const reader = new FileReader(); |
| reader.onload = (ev) => handleChange(ev.target.result); |
| reader.readAsText(file); |
| }, |
| [handleChange] |
| ); |
|
|
| const handlePasteExample = useCallback(() => { |
| handleChange(DEFAULT_YAML); |
| }, [handleChange]); |
|
|
| const errors = validateYAML(yamlText); |
|
|
| |
|
|
| const updateInterface = (idx, field, value) => { |
| const ifaces = [...(parsed.interfaces || [])]; |
| ifaces[idx] = { ...ifaces[idx], [field]: value }; |
| const updated = { ...parsed, interfaces: ifaces }; |
| setParsed(updated); |
| handleChange(toYAML(updated)); |
| }; |
|
|
| const addSignal = (ifaceIdx) => { |
| const ifaces = [...(parsed.interfaces || [])]; |
| const existing = Array.isArray(ifaces[ifaceIdx]?.signals) ? ifaces[ifaceIdx].signals : []; |
| const signals = [...existing, { name: "", direction: "input", width: 1 }]; |
| ifaces[ifaceIdx] = { ...ifaces[ifaceIdx], signals }; |
| const updated = { ...parsed, interfaces: ifaces }; |
| setParsed(updated); |
| handleChange(toYAML(updated)); |
| }; |
|
|
| const updateSignal = (ifaceIdx, sigIdx, field, value) => { |
| const ifaces = [...(parsed.interfaces || [])]; |
| const existing = Array.isArray(ifaces[ifaceIdx]?.signals) ? ifaces[ifaceIdx].signals : []; |
| const signals = [...existing]; |
| signals[sigIdx] = { ...signals[sigIdx], [field]: value }; |
| ifaces[ifaceIdx] = { ...ifaces[ifaceIdx], signals }; |
| const updated = { ...parsed, interfaces: ifaces }; |
| setParsed(updated); |
| handleChange(toYAML(updated)); |
| }; |
|
|
| const removeSignal = (ifaceIdx, sigIdx) => { |
| const ifaces = [...(parsed.interfaces || [])]; |
| const sigs = Array.isArray(ifaces[ifaceIdx].signals) ? ifaces[ifaceIdx].signals : []; |
| ifaces[ifaceIdx] = { |
| ...ifaces[ifaceIdx], |
| signals: sigs.filter((_, i) => i !== sigIdx), |
| }; |
| const updated = { ...parsed, interfaces: ifaces }; |
| setParsed(updated); |
| handleChange(toYAML(updated)); |
| }; |
|
|
| const addRegister = () => { |
| const regs = [...(parsed.registers || []), { name: "", address: "0x00", access: "rw", fields: [] }]; |
| const updated = { ...parsed, registers: regs }; |
| setParsed(updated); |
| handleChange(toYAML(updated)); |
| }; |
|
|
| const updateRegister = (idx, field, value) => { |
| const regs = [...(parsed.registers || [])]; |
| regs[idx] = { ...regs[idx], [field]: value }; |
| const updated = { ...parsed, registers: regs }; |
| setParsed(updated); |
| handleChange(toYAML(updated)); |
| }; |
|
|
| const removeRegister = (idx) => { |
| const updated = { ...parsed, registers: parsed.registers.filter((_, i) => i !== idx) }; |
| setParsed(updated); |
| handleChange(toYAML(updated)); |
| }; |
|
|
| const addField = (regIdx) => { |
| const regs = [...(parsed.registers || [])]; |
| const fields = [...(regs[regIdx]?.fields || []), { name: "", bits: "0", description: "" }]; |
| regs[regIdx] = { ...regs[regIdx], fields }; |
| const updated = { ...parsed, registers: regs }; |
| setParsed(updated); |
| handleChange(toYAML(updated)); |
| }; |
|
|
| const updateField = (regIdx, fldIdx, field, value) => { |
| const regs = [...(parsed.registers || [])]; |
| const fields = [...(regs[regIdx]?.fields || [])]; |
| fields[fldIdx] = { ...fields[fldIdx], [field]: value }; |
| regs[regIdx] = { ...regs[regIdx], fields }; |
| const updated = { ...parsed, registers: regs }; |
| setParsed(updated); |
| handleChange(toYAML(updated)); |
| }; |
|
|
| const removeField = (regIdx, fldIdx) => { |
| const regs = [...(parsed.registers || [])]; |
| regs[regIdx] = { |
| ...regs[regIdx], |
| fields: regs[regIdx].fields.filter((_, i) => i !== fldIdx), |
| }; |
| const updated = { ...parsed, registers: regs }; |
| setParsed(updated); |
| handleChange(toYAML(updated)); |
| }; |
|
|
| return ( |
| <div className="card"> |
| <div className="card-header"> |
| <FileCode size={18} className="text-brand-600" /> |
| <h2>Design Specification</h2> |
| </div> |
| |
| <div className="card-body space-y-4"> |
| {/* Toolbar */} |
| <div className="flex flex-wrap items-center gap-2"> |
| <button className="btn-secondary text-xs" onClick={() => fileRef.current?.click()}> |
| <Upload size={14} /> Upload .core / .yaml |
| </button> |
| <input |
| ref={fileRef} |
| type="file" |
| accept=".yaml,.yml,.core,.json" |
| className="hidden" |
| onChange={handleFileUpload} |
| /> |
| <button className="btn-secondary text-xs" onClick={handlePasteExample}> |
| Load Example |
| </button> |
| <div className="ml-auto flex items-center gap-1 bg-slate-100 rounded-lg p-0.5"> |
| {["spec", "signals", "registers"].map((t) => ( |
| <button |
| key={t} |
| onClick={() => setActiveTab(t)} |
| className={`px-3 py-1.5 text-xs font-medium rounded-md transition-colors ${ |
| activeTab === t |
| ? "bg-white text-slate-900 shadow-sm" |
| : "text-slate-500 hover:text-slate-700" |
| }`} |
| > |
| {t === "spec" ? "Spec" : t === "signals" ? "Signals" : "Registers"} |
| </button> |
| ))} |
| </div> |
| </div> |
| |
| {/* Validation status */} |
| {errors.length > 0 && ( |
| <div className="rounded-lg bg-red-50 border border-red-200 px-4 py-3"> |
| <div className="flex items-start gap-2"> |
| <AlertCircle size={16} className="text-red-500 mt-0.5 shrink-0" /> |
| <div className="text-xs text-red-700 space-y-0.5"> |
| {errors.map((e, i) => ( |
| <p key={i}>{e}</p> |
| ))} |
| </div> |
| </div> |
| </div> |
| )} |
| {errors.length === 0 && yamlText.trim() && ( |
| <div className="flex items-center gap-2 text-xs text-emerald-600"> |
| <CheckCircle2 size={14} /> |
| YAML syntax valid |
| </div> |
| )} |
| |
| {/* ββ Spec tab βββββββββββββββββββββββββββββββββββ */} |
| {activeTab === "spec" && ( |
| <div className="space-y-3"> |
| <div className="grid grid-cols-1 sm:grid-cols-3 gap-3"> |
| <div> |
| <label className="block text-xs font-medium text-slate-600 mb-1">Design Name</label> |
| <input |
| className="input-field text-sm" |
| value={parsed.design_name || ""} |
| onChange={(e) => { |
| const updated = { ...parsed, design_name: e.target.value }; |
| setParsed(updated); |
| handleChange(toYAML(updated)); |
| }} |
| placeholder="uart16550" |
| /> |
| </div> |
| <div> |
| <label className="block text-xs font-medium text-slate-600 mb-1">Version</label> |
| <input |
| className="input-field text-sm" |
| value={parsed.version || ""} |
| onChange={(e) => { |
| const updated = { ...parsed, version: e.target.value }; |
| setParsed(updated); |
| handleChange(toYAML(updated)); |
| }} |
| placeholder="1.5" |
| /> |
| </div> |
| <div> |
| <label className="block text-xs font-medium text-slate-600 mb-1">Vendor</label> |
| <input |
| className="input-field text-sm" |
| value={parsed.vendor || ""} |
| onChange={(e) => { |
| const updated = { ...parsed, vendor: e.target.value }; |
| setParsed(updated); |
| handleChange(toYAML(updated)); |
| }} |
| placeholder="DV Automation" |
| /> |
| </div> |
| </div> |
| |
| <div> |
| <label className="block text-xs font-medium text-slate-600 mb-1">Description</label> |
| <textarea |
| className="input-field text-sm resize-none" |
| rows={2} |
| value={parsed.description || ""} |
| onChange={(e) => { |
| const updated = { ...parsed, description: e.target.value }; |
| setParsed(updated); |
| handleChange(toYAML(updated)); |
| }} |
| placeholder="Core description" |
| /> |
| </div> |
| |
| <div className="grid grid-cols-1 sm:grid-cols-2 gap-3"> |
| <div> |
| <label className="block text-xs font-medium text-slate-600 mb-1">Protocol</label> |
| <select |
| className="select-field text-sm" |
| value={parsed.protocol || ""} |
| onChange={(e) => { |
| const updated = { ...parsed, protocol: e.target.value }; |
| setParsed(updated); |
| handleChange(toYAML(updated)); |
| }} |
| > |
| <option value="">Auto-detect</option> |
| {PROTOCOLS.map((p) => ( |
| <option key={p} value={p}>{p.toUpperCase()}</option> |
| ))} |
| </select> |
| </div> |
| <div> |
| <label className="block text-xs font-medium text-slate-600 mb-1">Language</label> |
| <select |
| className="select-field text-sm" |
| value={parsed.language || "systemverilog"} |
| onChange={(e) => { |
| const updated = { ...parsed, language: e.target.value }; |
| setParsed(updated); |
| handleChange(toYAML(updated)); |
| }} |
| > |
| <option value="systemverilog">SystemVerilog</option> |
| <option value="verilog">Verilog</option> |
| <option value="vhdl">VHDL</option> |
| </select> |
| </div> |
| </div> |
| |
| <div className="grid grid-cols-1 sm:grid-cols-3 gap-3"> |
| <div> |
| <label className="block text-xs font-medium text-slate-600 mb-1">Clock (clk)</label> |
| <input className="input-field text-sm" value={parsed.clock_reset?.clock || "clk"} onChange={(e) => { |
| const cr = { ...(parsed.clock_reset || {}), clock: e.target.value }; |
| const updated = { ...parsed, clock_reset: cr }; |
| setParsed(updated); handleChange(toYAML(updated)); |
| }} /> |
| </div> |
| <div> |
| <label className="block text-xs font-medium text-slate-600 mb-1">Reset (rst_n)</label> |
| <input className="input-field text-sm" value={parsed.clock_reset?.reset || "rst_n"} onChange={(e) => { |
| const cr = { ...(parsed.clock_reset || {}), reset: e.target.value }; |
| const updated = { ...parsed, clock_reset: cr }; |
| setParsed(updated); handleChange(toYAML(updated)); |
| }} /> |
| </div> |
| <div> |
| <label className="block text-xs font-medium text-slate-600 mb-1">Reset Active</label> |
| <select className="select-field text-sm" value={parsed.clock_reset?.reset_active ?? 0} onChange={(e) => { |
| const cr = { ...(parsed.clock_reset || {}), reset_active: Number(e.target.value) }; |
| const updated = { ...parsed, clock_reset: cr }; |
| setParsed(updated); handleChange(toYAML(updated)); |
| }}> |
| <option value={0}>Active Low (0)</option> |
| <option value={1}>Active High (1)</option> |
| </select> |
| </div> |
| </div> |
| |
| {/* Raw YAML editor */} |
| <div> |
| <label className="block text-xs font-medium text-slate-600 mb-1"> |
| Raw YAML / .core Editor |
| </label> |
| <textarea |
| className="input-field text-xs font-mono resize-y" |
| rows={10} |
| value={yamlText} |
| onChange={(e) => handleChange(e.target.value)} |
| spellCheck={false} |
| /> |
| </div> |
| </div> |
| )} |
| |
| {/* ββ Signals tab βββββββββββββββββββββββββββββββββββ */} |
| {activeTab === "signals" && ( |
| <div className="space-y-4"> |
| {(parsed.interfaces || []).length === 0 && ( |
| <p className="text-xs text-slate-400">No interfaces defined. Add one in the Spec tab.</p> |
| )} |
| {(parsed.interfaces || []).map((iface, iIdx) => ( |
| <div key={iIdx} className="rounded-lg border border-slate-200 p-4 space-y-3"> |
| <div className="flex items-center justify-between"> |
| <div className="flex items-center gap-2"> |
| <span className="badge-info">{iface.name || `Interface #${iIdx + 1}`}</span> |
| <select |
| className="select-field text-xs py-1 w-28" |
| value={iface.protocol || ""} |
| onChange={(e) => updateInterface(iIdx, "protocol", e.target.value)} |
| > |
| <option value="">Protocol</option> |
| {PROTOCOLS.map((p) => ( |
| <option key={p} value={p}>{p.toUpperCase()}</option> |
| ))} |
| </select> |
| </div> |
| </div> |
| |
| <div className="space-y-2"> |
| {Array.isArray(iface.signals) && iface.signals.map((sig, sIdx) => ( |
| <div key={sIdx} className="flex items-center gap-2"> |
| <input |
| className="input-field text-xs py-1.5 w-28" |
| value={sig.name} |
| onChange={(e) => updateSignal(iIdx, sIdx, "name", e.target.value)} |
| placeholder="signal_name" |
| /> |
| <select |
| className="select-field text-xs py-1.5 w-22" |
| value={sig.direction} |
| onChange={(e) => updateSignal(iIdx, sIdx, "direction", e.target.value)} |
| > |
| <option value="input">input</option> |
| <option value="output">output</option> |
| <option value="inout">inout</option> |
| </select> |
| <div className="flex items-center gap-1"> |
| <span className="text-[11px] text-slate-500">w:</span> |
| <input |
| className="input-field text-xs py-1.5 w-16 text-center" |
| type="number" |
| min={1} |
| value={sig.width || 1} |
| onChange={(e) => updateSignal(iIdx, sIdx, "width", Number(e.target.value))} |
| /> |
| </div> |
| <button |
| className="p-1.5 rounded-md text-slate-400 hover:text-red-500 hover:bg-red-50 transition-colors" |
| onClick={() => removeSignal(iIdx, sIdx)} |
| title="Remove signal" |
| > |
| <Trash2 size={14} /> |
| </button> |
| </div> |
| ))} |
| </div> |
| <button |
| className="btn-secondary text-xs" |
| onClick={() => addSignal(iIdx)} |
| > |
| <Plus size={14} /> Add Signal |
| </button> |
| </div> |
| ))} |
| </div> |
| )} |
| |
| {/* ββ Registers tab βββββββββββββββββββββββββββββββββββ */} |
| {activeTab === "registers" && ( |
| <div className="space-y-4"> |
| {(parsed.registers || []).length === 0 && ( |
| <p className="text-xs text-slate-400">No registers defined.</p> |
| )} |
| {(parsed.registers || []).map((reg, rIdx) => ( |
| <div key={rIdx} className="rounded-lg border border-slate-200 p-4 space-y-3"> |
| <div className="flex items-center gap-2 flex-wrap"> |
| <input |
| className="input-field text-xs py-1.5 w-28" |
| value={reg.name} |
| onChange={(e) => updateRegister(rIdx, "name", e.target.value)} |
| placeholder="REG_NAME" |
| /> |
| <input |
| className="input-field text-xs py-1.5 w-22 font-mono" |
| value={reg.address} |
| onChange={(e) => updateRegister(rIdx, "address", e.target.value)} |
| placeholder="0x00" |
| /> |
| <select |
| className="select-field text-xs py-1.5 w-20" |
| value={reg.access || "rw"} |
| onChange={(e) => updateRegister(rIdx, "access", e.target.value)} |
| > |
| <option value="rw">rw</option> |
| <option value="ro">ro</option> |
| <option value="wo">wo</option> |
| <option value="rc">rc</option> |
| <option value="w1c">w1c</option> |
| </select> |
| <input |
| className="input-field text-xs py-1.5 w-32" |
| value={reg.description || ""} |
| onChange={(e) => updateRegister(rIdx, "description", e.target.value)} |
| placeholder="Description" |
| /> |
| <button |
| className="p-1.5 rounded-md text-slate-400 hover:text-red-500 hover:bg-red-50 transition-colors ml-auto" |
| onClick={() => removeRegister(rIdx)} |
| title="Remove register" |
| > |
| <Trash2 size={14} /> |
| </button> |
| </div> |
| |
| {/* Fields */} |
| {(reg.fields || []).length > 0 && ( |
| <div className="ml-2 pl-3 border-l-2 border-brand-200 space-y-2"> |
| <span className="text-[11px] font-medium text-slate-500 uppercase tracking-wider"> |
| Fields |
| </span> |
| {(reg.fields || []).map((fld, fIdx) => ( |
| <div key={fIdx} className="flex items-center gap-2"> |
| <input |
| className="input-field text-xs py-1 w-24" |
| value={fld.name} |
| onChange={(e) => updateField(rIdx, fIdx, "name", e.target.value)} |
| placeholder="field_name" |
| /> |
| <input |
| className="input-field text-xs py-1 w-16 font-mono text-center" |
| value={fld.bits} |
| onChange={(e) => updateField(rIdx, fIdx, "bits", e.target.value)} |
| placeholder="7:0" |
| /> |
| <input |
| className="input-field text-xs py-1 flex-1" |
| value={fld.description || ""} |
| onChange={(e) => updateField(rIdx, fIdx, "description", e.target.value)} |
| placeholder="Description" |
| /> |
| <button |
| className="p-1 rounded-md text-slate-400 hover:text-red-500 hover:bg-red-50" |
| onClick={() => removeField(rIdx, fIdx)} |
| > |
| <Trash2 size={12} /> |
| </button> |
| </div> |
| ))} |
| </div> |
| )} |
| <button className="btn-secondary text-xs" onClick={() => addField(rIdx)}> |
| <Plus size={14} /> Add Field |
| </button> |
| </div> |
| ))} |
| <button className="btn-secondary text-xs" onClick={addRegister}> |
| <Plus size={14} /> Add Register |
| </button> |
| </div> |
| )} |
| </div> |
| </div> |
| ); |
| } |
|
|