Flow Meter: varighetsindikator for storyboard
Ny FlowMeter-komponent som viser episodeprogresjon som en fargekodet linje (rød→gul→grønn) med pulsering nær mål. StoryboardTrait viser Flow Meter øverst og kort gruppert etter status (Klar, Tatt opp, Droppet). Ref: docs/proposals/flow_meter.md
This commit is contained in:
parent
68a00638f2
commit
4b8ce53777
4 changed files with 461 additions and 1 deletions
162
frontend/src/lib/components/traits/FlowMeter.svelte
Normal file
162
frontend/src/lib/components/traits/FlowMeter.svelte
Normal file
|
|
@ -0,0 +1,162 @@
|
||||||
|
<script lang="ts">
|
||||||
|
/**
|
||||||
|
* FlowMeter — Visuell varighetsindikator for storyboard.
|
||||||
|
*
|
||||||
|
* Tynn progresjonslinje som fylles basert på opptakstid vs. målvarighet.
|
||||||
|
* Fargeovergang: rød (0–30%) → gul (30–70%) → grønn (70–100%).
|
||||||
|
* Pulserer sakte når man nærmer seg mål (>85%).
|
||||||
|
*
|
||||||
|
* Ref: docs/proposals/flow_meter.md
|
||||||
|
*/
|
||||||
|
interface Props {
|
||||||
|
/** Målvarighet i minutter (f.eks. 45) */
|
||||||
|
targetMinutes: number;
|
||||||
|
/** Registrert/opptatt varighet i minutter */
|
||||||
|
recordedMinutes: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
let { targetMinutes, recordedMinutes }: Props = $props();
|
||||||
|
|
||||||
|
const progress = $derived(targetMinutes > 0 ? Math.min(recordedMinutes / targetMinutes, 1.5) : 0);
|
||||||
|
const percent = $derived(Math.min(progress * 100, 100));
|
||||||
|
const nearGoal = $derived(progress >= 0.85 && progress < 1.0);
|
||||||
|
const overGoal = $derived(progress >= 1.0);
|
||||||
|
|
||||||
|
/** Farge basert på progresjon: rød → gul → grønn */
|
||||||
|
const barColor = $derived.by(() => {
|
||||||
|
if (progress < 0.3) return '#ef4444'; // rød
|
||||||
|
if (progress < 0.7) return '#eab308'; // gul
|
||||||
|
return '#22c55e'; // grønn
|
||||||
|
});
|
||||||
|
|
||||||
|
function formatMinutes(mins: number): string {
|
||||||
|
const m = Math.floor(mins);
|
||||||
|
const s = Math.round((mins - m) * 60);
|
||||||
|
return s > 0 ? `${m}:${s.toString().padStart(2, '0')}` : `${m} min`;
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<div class="flow-meter" class:flow-meter--pulse={nearGoal} class:flow-meter--over={overGoal}>
|
||||||
|
<div class="flow-meter__track">
|
||||||
|
<div
|
||||||
|
class="flow-meter__fill"
|
||||||
|
style="width: {percent}%; background: {barColor};"
|
||||||
|
></div>
|
||||||
|
</div>
|
||||||
|
<div class="flow-meter__label">
|
||||||
|
<span class="flow-meter__time">{formatMinutes(recordedMinutes)}</span>
|
||||||
|
<span class="flow-meter__separator">/</span>
|
||||||
|
<span class="flow-meter__target">{formatMinutes(targetMinutes)}</span>
|
||||||
|
{#if overGoal}
|
||||||
|
<span class="flow-meter__badge flow-meter__badge--over">Over mål</span>
|
||||||
|
{:else if percent >= 70}
|
||||||
|
<span class="flow-meter__badge flow-meter__badge--good">Solid</span>
|
||||||
|
{:else if percent >= 30}
|
||||||
|
<span class="flow-meter__badge flow-meter__badge--mid">Trenger mer</span>
|
||||||
|
{:else}
|
||||||
|
<span class="flow-meter__badge flow-meter__badge--low">For kort</span>
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.flow-meter {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 4px;
|
||||||
|
padding: 8px 12px;
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.flow-meter__track {
|
||||||
|
height: 6px;
|
||||||
|
background: rgba(255, 255, 255, 0.08);
|
||||||
|
border-radius: 3px;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.flow-meter__fill {
|
||||||
|
height: 100%;
|
||||||
|
border-radius: 3px;
|
||||||
|
transition: width 0.4s ease, background 0.4s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.flow-meter--pulse .flow-meter__fill {
|
||||||
|
animation: flow-pulse 2s ease-in-out infinite;
|
||||||
|
}
|
||||||
|
|
||||||
|
.flow-meter--over .flow-meter__fill {
|
||||||
|
background: #22c55e !important;
|
||||||
|
box-shadow: 0 0 8px rgba(34, 197, 94, 0.4);
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes flow-pulse {
|
||||||
|
0%, 100% { opacity: 1; }
|
||||||
|
50% { opacity: 0.7; }
|
||||||
|
}
|
||||||
|
|
||||||
|
.flow-meter__label {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 4px;
|
||||||
|
font-size: 11px;
|
||||||
|
color: #8a8a96;
|
||||||
|
}
|
||||||
|
|
||||||
|
.flow-meter__time {
|
||||||
|
font-weight: 600;
|
||||||
|
color: #c4c4cc;
|
||||||
|
font-variant-numeric: tabular-nums;
|
||||||
|
}
|
||||||
|
|
||||||
|
.flow-meter__separator {
|
||||||
|
color: #4a4a54;
|
||||||
|
}
|
||||||
|
|
||||||
|
.flow-meter__target {
|
||||||
|
font-variant-numeric: tabular-nums;
|
||||||
|
}
|
||||||
|
|
||||||
|
.flow-meter__badge {
|
||||||
|
margin-left: auto;
|
||||||
|
font-size: 10px;
|
||||||
|
font-weight: 500;
|
||||||
|
padding: 1px 6px;
|
||||||
|
border-radius: 9999px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.flow-meter__badge--low {
|
||||||
|
background: rgba(239, 68, 68, 0.15);
|
||||||
|
color: #f87171;
|
||||||
|
}
|
||||||
|
|
||||||
|
.flow-meter__badge--mid {
|
||||||
|
background: rgba(234, 179, 8, 0.15);
|
||||||
|
color: #facc15;
|
||||||
|
}
|
||||||
|
|
||||||
|
.flow-meter__badge--good {
|
||||||
|
background: rgba(34, 197, 94, 0.15);
|
||||||
|
color: #4ade80;
|
||||||
|
}
|
||||||
|
|
||||||
|
.flow-meter__badge--over {
|
||||||
|
background: rgba(34, 197, 94, 0.2);
|
||||||
|
color: #4ade80;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Responsive: tighter on small panels */
|
||||||
|
@container (max-width: 300px) {
|
||||||
|
.flow-meter {
|
||||||
|
padding: 6px 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.flow-meter__label {
|
||||||
|
font-size: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.flow-meter__badge {
|
||||||
|
font-size: 9px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
292
frontend/src/lib/components/traits/StoryboardTrait.svelte
Normal file
292
frontend/src/lib/components/traits/StoryboardTrait.svelte
Normal file
|
|
@ -0,0 +1,292 @@
|
||||||
|
<script lang="ts">
|
||||||
|
/**
|
||||||
|
* StoryboardTrait — Storyboard-panel med Flow Meter.
|
||||||
|
*
|
||||||
|
* Viser en varighetsindikator (Flow Meter) øverst basert på kort
|
||||||
|
* med status "tatt_opp" som tilhører samlingen. Kort med
|
||||||
|
* metadata.duration_seconds bidrar til total opptakstid.
|
||||||
|
*
|
||||||
|
* Konfigurasjon via trait-config:
|
||||||
|
* { target_duration_minutes: 45 }
|
||||||
|
*
|
||||||
|
* Ref: docs/proposals/flow_meter.md, docs/proposals/storyboard.md
|
||||||
|
*/
|
||||||
|
import type { Node, Edge } from '$lib/realtime';
|
||||||
|
import { edgeStore, nodeStore, nodeVisibility } from '$lib/realtime';
|
||||||
|
import FlowMeter from './FlowMeter.svelte';
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
collection?: Node;
|
||||||
|
config: Record<string, unknown>;
|
||||||
|
userId?: string;
|
||||||
|
accessToken?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
let { collection, config, userId, accessToken }: Props = $props();
|
||||||
|
|
||||||
|
/** Målvarighet fra trait-config, default 45 minutter */
|
||||||
|
const targetMinutes = $derived(
|
||||||
|
typeof config.target_duration_minutes === 'number'
|
||||||
|
? config.target_duration_minutes
|
||||||
|
: 45
|
||||||
|
);
|
||||||
|
|
||||||
|
/** Alle kort som tilhører samlingen */
|
||||||
|
interface CardData {
|
||||||
|
node: Node;
|
||||||
|
status: string;
|
||||||
|
durationSeconds: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
const cards = $derived.by((): CardData[] => {
|
||||||
|
if (!collection?.id) return [];
|
||||||
|
|
||||||
|
const result: CardData[] = [];
|
||||||
|
|
||||||
|
for (const edge of edgeStore.byTarget(collection.id)) {
|
||||||
|
if (edge.edgeType !== 'belongs_to') continue;
|
||||||
|
|
||||||
|
const node = nodeStore.get(edge.sourceId);
|
||||||
|
if (!node || nodeVisibility(node, userId) === 'hidden') continue;
|
||||||
|
|
||||||
|
let status = 'klar';
|
||||||
|
for (const e of edgeStore.bySource(node.id)) {
|
||||||
|
if (e.edgeType === 'status' && e.targetId === collection.id) {
|
||||||
|
try {
|
||||||
|
const meta = JSON.parse(e.metadata ?? '{}');
|
||||||
|
if (meta.value) status = meta.value;
|
||||||
|
} catch { /* ignore */ }
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let durationSeconds = 0;
|
||||||
|
try {
|
||||||
|
const meta = JSON.parse(node.metadata ?? '{}');
|
||||||
|
if (typeof meta.duration_seconds === 'number') {
|
||||||
|
durationSeconds = meta.duration_seconds;
|
||||||
|
}
|
||||||
|
} catch { /* ignore */ }
|
||||||
|
|
||||||
|
result.push({ node, status, durationSeconds });
|
||||||
|
}
|
||||||
|
|
||||||
|
return result;
|
||||||
|
});
|
||||||
|
|
||||||
|
const cardsByStatus = $derived.by(() => {
|
||||||
|
const grouped: Record<string, CardData[]> = {
|
||||||
|
klar: [],
|
||||||
|
tatt_opp: [],
|
||||||
|
droppet: [],
|
||||||
|
};
|
||||||
|
for (const card of cards) {
|
||||||
|
const key = grouped[card.status] ? card.status : 'klar';
|
||||||
|
grouped[key].push(card);
|
||||||
|
}
|
||||||
|
return grouped;
|
||||||
|
});
|
||||||
|
|
||||||
|
/** Summert opptakstid for "Tatt opp"-kort, i minutter */
|
||||||
|
const recordedMinutes = $derived(
|
||||||
|
(cardsByStatus.tatt_opp ?? []).reduce((sum, c) => sum + c.durationSeconds, 0) / 60
|
||||||
|
);
|
||||||
|
|
||||||
|
const statusLabels: Record<string, string> = {
|
||||||
|
klar: 'Klar',
|
||||||
|
tatt_opp: 'Tatt opp',
|
||||||
|
droppet: 'Droppet',
|
||||||
|
};
|
||||||
|
|
||||||
|
const statusIcons: Record<string, string> = {
|
||||||
|
klar: '',
|
||||||
|
tatt_opp: '',
|
||||||
|
droppet: '',
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<div class="storyboard-trait">
|
||||||
|
<!-- Flow Meter — varighetsindikator -->
|
||||||
|
<FlowMeter {targetMinutes} {recordedMinutes} />
|
||||||
|
|
||||||
|
<!-- Kortliste gruppert etter status -->
|
||||||
|
<div class="storyboard-cards">
|
||||||
|
{#each ['tatt_opp', 'klar', 'droppet'] as status (status)}
|
||||||
|
{@const group = cardsByStatus[status] ?? []}
|
||||||
|
{#if group.length > 0}
|
||||||
|
<div class="storyboard-group">
|
||||||
|
<div class="storyboard-group-header">
|
||||||
|
<span class="storyboard-status-icon">{statusIcons[status]}</span>
|
||||||
|
<span class="storyboard-status-label">{statusLabels[status]}</span>
|
||||||
|
<span class="storyboard-status-count">{group.length}</span>
|
||||||
|
</div>
|
||||||
|
{#each group as card (card.node.id)}
|
||||||
|
<div class="storyboard-card storyboard-card--{status}">
|
||||||
|
<span class="storyboard-card-title">{card.node.title || 'Uten tittel'}</span>
|
||||||
|
{#if card.durationSeconds > 0}
|
||||||
|
<span class="storyboard-card-duration">
|
||||||
|
{Math.floor(card.durationSeconds / 60)}:{(card.durationSeconds % 60).toString().padStart(2, '0')}
|
||||||
|
</span>
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
{/each}
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
{/each}
|
||||||
|
|
||||||
|
{#if cards.length === 0}
|
||||||
|
<div class="storyboard-empty">
|
||||||
|
<p>Ingen kort ennå.</p>
|
||||||
|
<p class="storyboard-empty-hint">
|
||||||
|
Legg til kort med status for å se episodeprogresjonen i Flow Meter.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.storyboard-trait {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
height: 100%;
|
||||||
|
min-height: 0;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ================================================================= */
|
||||||
|
/* Card list */
|
||||||
|
/* ================================================================= */
|
||||||
|
.storyboard-cards {
|
||||||
|
flex: 1;
|
||||||
|
overflow-y: auto;
|
||||||
|
padding: 0 8px 8px;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.storyboard-group {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 3px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.storyboard-group-header {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 4px;
|
||||||
|
padding: 4px 6px;
|
||||||
|
font-size: 11px;
|
||||||
|
color: #8a8a96;
|
||||||
|
}
|
||||||
|
|
||||||
|
.storyboard-status-icon {
|
||||||
|
font-size: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.storyboard-status-label {
|
||||||
|
font-weight: 600;
|
||||||
|
text-transform: uppercase;
|
||||||
|
letter-spacing: 0.03em;
|
||||||
|
font-size: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.storyboard-status-count {
|
||||||
|
margin-left: auto;
|
||||||
|
font-size: 10px;
|
||||||
|
color: #5a5a66;
|
||||||
|
background: rgba(255, 255, 255, 0.06);
|
||||||
|
border-radius: 9999px;
|
||||||
|
padding: 0 5px;
|
||||||
|
line-height: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ================================================================= */
|
||||||
|
/* Card */
|
||||||
|
/* ================================================================= */
|
||||||
|
.storyboard-card {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 6px;
|
||||||
|
padding: 6px 8px;
|
||||||
|
background: rgba(255, 255, 255, 0.04);
|
||||||
|
border: 1px solid rgba(255, 255, 255, 0.06);
|
||||||
|
border-radius: 6px;
|
||||||
|
border-left: 3px solid transparent;
|
||||||
|
transition: background 0.1s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.storyboard-card:hover {
|
||||||
|
background: rgba(255, 255, 255, 0.07);
|
||||||
|
}
|
||||||
|
|
||||||
|
.storyboard-card--klar {
|
||||||
|
border-left-color: rgba(255, 255, 255, 0.2);
|
||||||
|
}
|
||||||
|
|
||||||
|
.storyboard-card--tatt_opp {
|
||||||
|
border-left-color: #22c55e;
|
||||||
|
}
|
||||||
|
|
||||||
|
.storyboard-card--droppet {
|
||||||
|
border-left-color: #ef4444;
|
||||||
|
opacity: 0.5;
|
||||||
|
}
|
||||||
|
|
||||||
|
.storyboard-card-title {
|
||||||
|
flex: 1;
|
||||||
|
font-size: 12px;
|
||||||
|
color: #c4c4cc;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.storyboard-card-duration {
|
||||||
|
font-size: 10px;
|
||||||
|
font-variant-numeric: tabular-nums;
|
||||||
|
color: #8a8a96;
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ================================================================= */
|
||||||
|
/* Empty state */
|
||||||
|
/* ================================================================= */
|
||||||
|
.storyboard-empty {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
flex: 1;
|
||||||
|
padding: 24px;
|
||||||
|
text-align: center;
|
||||||
|
color: #5a5a66;
|
||||||
|
font-size: 13px;
|
||||||
|
gap: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.storyboard-empty-hint {
|
||||||
|
font-size: 11px;
|
||||||
|
color: #4a4a54;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ================================================================= */
|
||||||
|
/* Responsive */
|
||||||
|
/* ================================================================= */
|
||||||
|
@container (max-width: 300px) {
|
||||||
|
.storyboard-card {
|
||||||
|
padding: 4px 6px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.storyboard-card-title {
|
||||||
|
font-size: 11px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 480px) {
|
||||||
|
.storyboard-cards {
|
||||||
|
padding: 0 6px 6px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
@ -58,6 +58,7 @@ export const TRAIT_PANEL_INFO: Record<string, TraitPanelInfo> = {
|
||||||
mindmap: { title: 'Tankekart', icon: '🧠', defaultWidth: 600, defaultHeight: 500 },
|
mindmap: { title: 'Tankekart', icon: '🧠', defaultWidth: 600, defaultHeight: 500 },
|
||||||
ai: { title: 'AI-verktøy', icon: '🤖', defaultWidth: 420, defaultHeight: 500 },
|
ai: { title: 'AI-verktøy', icon: '🤖', defaultWidth: 420, defaultHeight: 500 },
|
||||||
usage: { title: 'Ressursforbruk', icon: '📊', defaultWidth: 380, defaultHeight: 350 },
|
usage: { title: 'Ressursforbruk', icon: '📊', defaultWidth: 380, defaultHeight: 350 },
|
||||||
|
storyboard: { title: 'Storyboard', icon: '🎬', defaultWidth: 500, defaultHeight: 450 },
|
||||||
};
|
};
|
||||||
|
|
||||||
/** Default info for unknown traits */
|
/** Default info for unknown traits */
|
||||||
|
|
|
||||||
|
|
@ -36,6 +36,7 @@
|
||||||
import MixerTrait from '$lib/components/traits/MixerTrait.svelte';
|
import MixerTrait from '$lib/components/traits/MixerTrait.svelte';
|
||||||
import OrchestrationTrait from '$lib/components/traits/OrchestrationTrait.svelte';
|
import OrchestrationTrait from '$lib/components/traits/OrchestrationTrait.svelte';
|
||||||
import MindMapTrait from '$lib/components/traits/MindMapTrait.svelte';
|
import MindMapTrait from '$lib/components/traits/MindMapTrait.svelte';
|
||||||
|
import StoryboardTrait from '$lib/components/traits/StoryboardTrait.svelte';
|
||||||
import GenericTrait from '$lib/components/traits/GenericTrait.svelte';
|
import GenericTrait from '$lib/components/traits/GenericTrait.svelte';
|
||||||
import TraitAdmin from '$lib/components/traits/TraitAdmin.svelte';
|
import TraitAdmin from '$lib/components/traits/TraitAdmin.svelte';
|
||||||
|
|
||||||
|
|
@ -70,7 +71,7 @@
|
||||||
/** Traits with dedicated components */
|
/** Traits with dedicated components */
|
||||||
const knownTraits = new Set([
|
const knownTraits = new Set([
|
||||||
'editor', 'chat', 'kanban', 'podcast', 'publishing',
|
'editor', 'chat', 'kanban', 'podcast', 'publishing',
|
||||||
'rss', 'calendar', 'recording', 'transcription', 'studio', 'mixer', 'orchestration', 'mindmap'
|
'rss', 'calendar', 'recording', 'transcription', 'studio', 'mixer', 'orchestration', 'mindmap', 'storyboard'
|
||||||
]);
|
]);
|
||||||
|
|
||||||
/** Count of child nodes */
|
/** Count of child nodes */
|
||||||
|
|
@ -365,6 +366,8 @@
|
||||||
<OrchestrationTrait collection={collectionNode} config={traits[trait]} userId={nodeId} {accessToken} />
|
<OrchestrationTrait collection={collectionNode} config={traits[trait]} userId={nodeId} {accessToken} />
|
||||||
{:else if trait === 'mindmap'}
|
{:else if trait === 'mindmap'}
|
||||||
<MindMapTrait collection={collectionNode} config={traits[trait]} userId={nodeId} {accessToken} />
|
<MindMapTrait collection={collectionNode} config={traits[trait]} userId={nodeId} {accessToken} />
|
||||||
|
{:else if trait === 'storyboard'}
|
||||||
|
<StoryboardTrait collection={collectionNode} config={traits[trait]} userId={nodeId} {accessToken} />
|
||||||
{/if}
|
{/if}
|
||||||
{:else}
|
{:else}
|
||||||
<GenericTrait name={trait} config={traits[trait]} />
|
<GenericTrait name={trait} config={traits[trait]} />
|
||||||
|
|
@ -424,6 +427,8 @@
|
||||||
<OrchestrationTrait collection={collectionNode} config={traits[trait]} userId={nodeId} {accessToken} />
|
<OrchestrationTrait collection={collectionNode} config={traits[trait]} userId={nodeId} {accessToken} />
|
||||||
{:else if trait === 'mindmap'}
|
{:else if trait === 'mindmap'}
|
||||||
<MindMapTrait collection={collectionNode} config={traits[trait]} userId={nodeId} {accessToken} />
|
<MindMapTrait collection={collectionNode} config={traits[trait]} userId={nodeId} {accessToken} />
|
||||||
|
{:else if trait === 'storyboard'}
|
||||||
|
<StoryboardTrait collection={collectionNode} config={traits[trait]} userId={nodeId} {accessToken} />
|
||||||
{/if}
|
{/if}
|
||||||
{:else}
|
{:else}
|
||||||
<GenericTrait name={trait} config={traits[trait]} />
|
<GenericTrait name={trait} config={traits[trait]} />
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue