| 2629 |
2735
|
|
"""
|
| 2630 |
2736
|
|
end
|
| 2631 |
2737
|
|
|
|
2738
|
+ |
# ── AI conversation ───────────────────────────────────────────────────────
|
|
2739
|
+ |
# Ported from Vercel's AI Elements. Each page composes a whole family the way
|
|
2740
|
+ |
# a caller composes it, and shows the states the family exists to keep apart:
|
|
2741
|
+ |
# a tool call has seven, and a demo of one of them documents nothing.
|
|
2742
|
+ |
|
|
2743
|
+ |
defp component_demo(%{item: %{slug: "ai-conversation"}} = assigns) do
|
|
2744
|
+ |
~H"""
|
|
2745
|
+ |
<div class="space-y-3">
|
|
2746
|
+ |
<p class="text-sm text-base-content/60">
|
|
2747
|
+ |
Two boxes: the outer one positions and clips, the inner one scrolls. That split is
|
|
2748
|
+ |
what lets the scroll control hold still at the bottom edge while the transcript
|
|
2749
|
+ |
moves behind it. A colocated hook keeps the viewport pinned while the reader is
|
|
2750
|
+ |
already at the bottom, and lets go the moment they scroll up.
|
|
2751
|
+ |
</p>
|
|
2752
|
+ |
<Conversation.conversation
|
|
2753
|
+ |
id="demo-conversation"
|
|
2754
|
+ |
class="h-72 rounded-lg border border-border"
|
|
2755
|
+ |
>
|
|
2756
|
+ |
<Conversation.conversation_content class="space-y-6 p-4">
|
|
2757
|
+ |
<Conversation.message from="system">
|
|
2758
|
+ |
<Conversation.message_content text="Run `scv.13` opened on `OpenAgentsInc/openagents.com`." />
|
|
2759
|
+ |
</Conversation.message>
|
|
2760
|
+ |
<Conversation.message from="user">
|
|
2761
|
+ |
<Conversation.message_content text="Why did scv-13 revert its own patch?" />
|
|
2762
|
+ |
</Conversation.message>
|
|
2763
|
+ |
<Conversation.message from="assistant">
|
|
2764
|
+ |
<Conversation.message_avatar name="Sarah" />
|
|
2765
|
+ |
<Conversation.message_content text="`targets_test.exs` stayed red after the third attempt, so the run hit the revert rule in `SELF-EDIT-001` and put the worktree back where it found it." />
|
|
2766
|
+ |
</Conversation.message>
|
|
2767
|
+ |
<Conversation.message from="user">
|
|
2768
|
+ |
<Conversation.message_content text="Show me the third attempt." />
|
|
2769
|
+ |
</Conversation.message>
|
|
2770
|
+ |
<Conversation.message from="assistant">
|
|
2771
|
+ |
<Conversation.message_avatar name="Sarah" />
|
|
2772
|
+ |
<Conversation.message_content
|
|
2773
|
+ |
text="Reading the transcript for attempt three"
|
|
2774
|
+ |
streaming
|
|
2775
|
+ |
/>
|
|
2776
|
+ |
</Conversation.message>
|
|
2777
|
+ |
</Conversation.conversation_content>
|
|
2778
|
+ |
</Conversation.conversation>
|
|
2779
|
+ |
<p class="text-sm text-base-content/60">
|
|
2780
|
+ |
With nothing in it the same scroller carries a placeholder rather than an empty
|
|
2781
|
+ |
box, and the scroll control is left off, because there is nowhere to go.
|
|
2782
|
+ |
</p>
|
|
2783
|
+ |
<Conversation.conversation
|
|
2784
|
+ |
id="demo-conversation-empty"
|
|
2785
|
+ |
scroll_button={false}
|
|
2786
|
+ |
class="h-44 rounded-lg border border-border"
|
|
2787
|
+ |
>
|
|
2788
|
+ |
<Conversation.conversation_content class="grid h-full place-items-center p-4">
|
|
2789
|
+ |
<Conversation.conversation_empty_state
|
|
2790
|
+ |
icon="chat"
|
|
2791
|
+ |
title="No messages yet"
|
|
2792
|
+ |
description="Ask about a repository, or start an SCV run."
|
|
2793
|
+ |
/>
|
|
2794
|
+ |
</Conversation.conversation_content>
|
|
2795
|
+ |
</Conversation.conversation>
|
|
2796
|
+ |
</div>
|
|
2797
|
+ |
"""
|
|
2798
|
+ |
end
|
|
2799
|
+ |
|
|
2800
|
+ |
defp component_demo(%{item: %{slug: "ai-message"}} = assigns) do
|
|
2801
|
+ |
~H"""
|
|
2802
|
+ |
<div class="space-y-3">
|
|
2803
|
+ |
<p class="text-sm text-base-content/60">
|
|
2804
|
+ |
A turn is a row of siblings the caller orders, not one component with a slot per
|
|
2805
|
+ |
position: an assistant turn wants a face, a body, and controls; a user turn wants
|
|
2806
|
+ |
only a body, and dropping a part should not mean passing an empty slot. The <code>from</code>
|
|
2807
|
+ |
attribute lands as the marker class every rule inside the body reads, which is
|
|
2808
|
+ |
what moves a user turn to the right and gives it a bubble.
|
|
2809
|
+ |
</p>
|
|
2810
|
+ |
<div class="space-y-6">
|
|
2811
|
+ |
<Conversation.message id="demo-message-user" from="user">
|
|
2812
|
+ |
<Conversation.message_content text="Which module decides whether an SCV run is admitted?" />
|
|
2813
|
+ |
</Conversation.message>
|
|
2814
|
+ |
|
|
2815
|
+ |
<Conversation.message id="demo-message-assistant" from="assistant">
|
|
2816
|
+ |
<Conversation.message_avatar name="Sarah" />
|
|
2817
|
+ |
<Conversation.message_content text="`OpenAgents.Forge.Targets` scores the candidates and `Placement` admits one. The refusal path is the interesting half: an expired capability manifest is refused before scoring, so it never reaches the queue." />
|
|
2818
|
+ |
<Conversation.message_actions>
|
|
2819
|
+ |
<Conversation.message_action label="Copy">
|
|
2820
|
+ |
<UI.icon name="copy" class="size-4" />
|
|
2821
|
+ |
</Conversation.message_action>
|
|
2822
|
+ |
<Conversation.message_action label="Regenerate">
|
|
2823
|
+ |
<UI.icon name="regenerate" class="size-4" />
|
|
2824
|
+ |
</Conversation.message_action>
|
|
2825
|
+ |
<Conversation.message_action label="Good answer">
|
|
2826
|
+ |
<UI.icon name="thumb-up" class="size-4" />
|
|
2827
|
+ |
</Conversation.message_action>
|
|
2828
|
+ |
<Conversation.message_action label="Bad answer">
|
|
2829
|
+ |
<UI.icon name="thumb-down" class="size-4" />
|
|
2830
|
+ |
</Conversation.message_action>
|
|
2831
|
+ |
</Conversation.message_actions>
|
|
2832
|
+ |
</Conversation.message>
|
|
2833
|
+ |
|
|
2834
|
+ |
<Conversation.message id="demo-message-streaming" from="assistant">
|
|
2835
|
+ |
<Conversation.message_avatar name="Sarah" />
|
|
2836
|
+ |
<Conversation.message_content
|
|
2837
|
+ |
text="Checking whether `Placement.admit/2` is still"
|
|
2838
|
+ |
streaming
|
|
2839
|
+ |
/>
|
|
2840
|
+ |
</Conversation.message>
|
|
2841
|
+ |
|
|
2842
|
+ |
<Conversation.message id="demo-message-system" from="system">
|
|
2843
|
+ |
<Conversation.message_content text="Budget window exhausted. The run resumes at the next window." />
|
|
2844
|
+ |
</Conversation.message>
|
|
2845
|
+ |
</div>
|
|
2846
|
+ |
<p class="text-sm text-base-content/60">
|
|
2847
|
+ |
The streaming turn is the one worth reading twice. Its text ends mid-sentence and
|
|
2848
|
+ |
still renders, because <code>streaming</code>
|
|
2849
|
+ |
tells the Markdown renderer to close what the model has not closed yet.
|
|
2850
|
+ |
</p>
|
|
2851
|
+ |
</div>
|
|
2852
|
+ |
"""
|
|
2853
|
+ |
end
|
|
2854
|
+ |
|
|
2855
|
+ |
defp component_demo(%{item: %{slug: "ai-shimmer"}} = assigns) do
|
|
2856
|
+ |
~H"""
|
|
2857
|
+ |
<div class="space-y-3">
|
|
2858
|
+ |
<p class="text-sm text-base-content/60">
|
|
2859
|
+ |
A highlight travelling through the text rather than a spinner beside it: the words
|
|
2860
|
+ |
stay readable the whole time, so waiting says what it is waiting for. <code>spread</code>
|
|
2861
|
+ |
widens the band, which is how a long line and a short one can take the same time
|
|
2862
|
+ |
to sweep.
|
|
2863
|
+ |
</p>
|
|
2864
|
+ |
<div class="space-y-3">
|
|
2865
|
+ |
<Conversation.shimmer id="demo-shimmer-short" text="Thinking" />
|
|
2866
|
+ |
<Conversation.shimmer
|
|
2867
|
+ |
id="demo-shimmer-line"
|
|
2868
|
+ |
text="Reading lib/openagents/forge/targets.ex"
|
|
2869
|
+ |
/>
|
|
2870
|
+ |
<Conversation.shimmer
|
|
2871
|
+ |
id="demo-shimmer-wide"
|
|
2872
|
+ |
text="Planning the change across four files"
|
|
2873
|
+ |
spread={4}
|
|
2874
|
+ |
/>
|
|
2875
|
+ |
<Conversation.shimmer
|
|
2876
|
+ |
id="demo-shimmer-heading"
|
|
2877
|
+ |
text="Working"
|
|
2878
|
+ |
tag="h3"
|
|
2879
|
+ |
class="text-lg font-medium"
|
|
2880
|
+ |
/>
|
|
2881
|
+ |
</div>
|
|
2882
|
+ |
</div>
|
|
2883
|
+ |
"""
|
|
2884
|
+ |
end
|
|
2885
|
+ |
|
|
2886
|
+ |
defp component_demo(%{item: %{slug: "ai-suggestions"}} = assigns) do
|
|
2887
|
+ |
~H"""
|
|
2888
|
+ |
<div class="space-y-3">
|
|
2889
|
+ |
<p class="text-sm text-base-content/60">
|
|
2890
|
+ |
Openers a reader can send unedited. The row scrolls sideways rather than wrapping,
|
|
2891
|
+ |
because a wrapped set changes height as it changes length, and a control that
|
|
2892
|
+ |
moves the composer down the page every time the model finishes is worse than one
|
|
2893
|
+ |
the reader has to scroll.
|
|
2894
|
+ |
</p>
|
|
2895
|
+ |
<Conversation.suggestions id="demo-suggestions">
|
|
2896
|
+ |
<Conversation.suggestion suggestion="What changed in this repository today?" />
|
|
2897
|
+ |
<Conversation.suggestion suggestion="Why did the last SCV run refuse?" />
|
|
2898
|
+ |
<Conversation.suggestion suggestion="Open an issue for the flaky targets test" />
|
|
2899
|
+ |
<Conversation.suggestion suggestion="Summarise the staging deploy" variant={:secondary} />
|
|
2900
|
+ |
<Conversation.suggestion suggestion="Show me the diff" variant={:ghost} size={:xs} />
|
|
2901
|
+ |
</Conversation.suggestions>
|
|
2902
|
+ |
</div>
|
|
2903
|
+ |
"""
|
|
2904
|
+ |
end
|
|
2905
|
+ |
|
|
2906
|
+ |
defp component_demo(%{item: %{slug: "ai-toolbar"}} = assigns) do
|
|
2907
|
+ |
~H"""
|
|
2908
|
+ |
<div class="space-y-3">
|
|
2909
|
+ |
<p class="text-sm text-base-content/60">
|
|
2910
|
+ |
The bar that floats over a transcript. It takes a <code>label</code>
|
|
2911
|
+ |
because a bar of icon buttons with no name is a set of unexplained glyphs to
|
|
2912
|
+ |
anyone not looking at it.
|
|
2913
|
+ |
</p>
|
|
2914
|
+ |
<div class="relative h-40 rounded-lg border border-border bg-muted/30">
|
|
2915
|
+ |
<Conversation.toolbar id="demo-toolbar" label="Transcript actions">
|
|
2916
|
+ |
<Conversation.message_action label="Search this run">
|
|
2917
|
+ |
<UI.icon name="search" class="size-4" />
|
|
2918
|
+ |
</Conversation.message_action>
|
|
2919
|
+ |
<Conversation.message_action label="Export transcript">
|
|
2920
|
+ |
<UI.icon name="download" class="size-4" />
|
|
2921
|
+ |
</Conversation.message_action>
|
|
2922
|
+ |
<Conversation.message_action label="Share run">
|
|
2923
|
+ |
<UI.icon name="share" class="size-4" />
|
|
2924
|
+ |
</Conversation.message_action>
|
|
2925
|
+ |
</Conversation.toolbar>
|
|
2926
|
+ |
</div>
|
|
2927
|
+ |
</div>
|
|
2928
|
+ |
"""
|
|
2929
|
+ |
end
|
|
2930
|
+ |
|
|
2931
|
+ |
defp component_demo(%{item: %{slug: "ai-controls"}} = assigns) do
|
|
2932
|
+ |
~H"""
|
|
2933
|
+ |
<div class="space-y-3">
|
|
2934
|
+ |
<p class="text-sm text-base-content/60">
|
|
2935
|
+ |
The same idea one level down: a group of actions that has to stay legible over
|
|
2936
|
+ |
whatever it sits on. Here it sits over the transcript ground rather than the page
|
|
2937
|
+ |
ground, which is the case the treatment exists for.
|
|
2938
|
+ |
</p>
|
|
2939
|
+ |
<div class="relative h-40 rounded-lg border border-border bg-muted/30 p-4">
|
|
2940
|
+ |
<Conversation.controls id="demo-controls" label="Run controls">
|
|
2941
|
+ |
<UI.button variant={:ghost} size={:sm}>
|
|
2942
|
+ |
<UI.icon name="pause" class="size-4" /> Pause
|
|
2943
|
+ |
</UI.button>
|
|
2944
|
+ |
<UI.button variant={:ghost} size={:sm}>
|
|
2945
|
+ |
<UI.icon name="stop" class="size-4" /> Stop
|
|
2946
|
+ |
</UI.button>
|
|
2947
|
+ |
<UI.button variant={:ghost} size={:sm}>
|
|
2948
|
+ |
<UI.icon name="arrow-rotate-cw" class="size-4" /> Retry
|
|
2949
|
+ |
</UI.button>
|
|
2950
|
+ |
</Conversation.controls>
|
|
2951
|
+ |
</div>
|
|
2952
|
+ |
</div>
|
|
2953
|
+ |
"""
|
|
2954
|
+ |
end
|
|
2955
|
+ |
|
|
2956
|
+ |
defp component_demo(%{item: %{slug: "ai-persona"}} = assigns) do
|
|
2957
|
+ |
~H"""
|
|
2958
|
+ |
<div class="space-y-3">
|
|
2959
|
+ |
<p class="text-sm text-base-content/60">
|
|
2960
|
+ |
Upstream this is a WebGL canvas playing one of six remote animations. What
|
|
2961
|
+ |
survived the port is the contract: a fixed set of states, one presence marker, and
|
|
2962
|
+ |
a name. Two states borrow a marker rather than introduce a colour — <code>thinking</code>
|
|
2963
|
+ |
reads as running and <code>asleep</code>
|
|
2964
|
+ |
as ended — and the marker is decorative, because the word beside it already says
|
|
2965
|
+ |
it.
|
|
2966
|
+ |
</p>
|
|
2967
|
+ |
<div class="grid gap-4 sm:grid-cols-2">
|
|
2968
|
+ |
<Conversation.persona id="demo-persona-idle" name="Sarah" state="idle" />
|
|
2969
|
+ |
<Conversation.persona id="demo-persona-listening" name="Sarah" state="listening" />
|
|
2970
|
+ |
<Conversation.persona id="demo-persona-thinking" name="Sarah" state="thinking" />
|
|
2971
|
+ |
<Conversation.persona id="demo-persona-speaking" name="Sarah" state="speaking" />
|
|
2972
|
+ |
<Conversation.persona id="demo-persona-asleep" name="Sarah" state="asleep" />
|
|
2973
|
+ |
<Conversation.persona
|
|
2974
|
+ |
id="demo-persona-labelled"
|
|
2975
|
+ |
name="scv-13"
|
|
2976
|
+ |
state="thinking"
|
|
2977
|
+ |
status_label="Reverting the third attempt"
|
|
2978
|
+ |
/>
|
|
2979
|
+ |
</div>
|
|
2980
|
+ |
</div>
|
|
2981
|
+ |
"""
|
|
2982
|
+ |
end
|
|
2983
|
+ |
|
|
2984
|
+ |
# ── AI reasoning ──────────────────────────────────────────────────────────
|
|
2985
|
+ |
|
|
2986
|
+ |
defp component_demo(%{item: %{slug: "ai-reasoning"}} = assigns) do
|
|
2987
|
+ |
~H"""
|
|
2988
|
+ |
<div class="space-y-4">
|
|
2989
|
+ |
<p class="text-sm text-base-content/60">
|
|
2990
|
+ |
A native <code>details</code>, so the disclosure needs no script and carries its
|
|
2991
|
+ |
own <code>aria-expanded</code>. The server writes the initial state, which keeps
|
|
2992
|
+ |
"open while it streams, closed once it lands" a LiveView decision: pass
|
|
2993
|
+ |
<code phx-no-curly-interpolation>open={@streaming}</code>
|
|
2994
|
+ |
and re-render. A reader who toggles it owns it until the next render of that
|
|
2995
|
+ |
attribute.
|
|
2996
|
+ |
</p>
|
|
2997
|
+ |
<Reasoning.reasoning id="demo-reasoning-open" open>
|
|
2998
|
+ |
<Reasoning.reasoning_trigger streaming />
|
|
2999
|
+ |
<Reasoning.reasoning_content
|
|
3000
|
+ |
text="The question is about admission, not scoring. `Targets` ranks candidates, but the refusal happens earlier"
|
|
3001
|
+ |
streaming
|
|
3002
|
+ |
/>
|
|
3003
|
+ |
</Reasoning.reasoning>
|
|
3004
|
+ |
<Reasoning.reasoning id="demo-reasoning-closed">
|
|
3005
|
+ |
<Reasoning.reasoning_trigger duration={12} />
|
|
3006
|
+ |
<Reasoning.reasoning_content text="The question is about admission, not scoring. `Targets` ranks candidates, but the refusal happens earlier, in the capability check, so an expired manifest never reaches the queue." />
|
|
3007
|
+ |
</Reasoning.reasoning>
|
|
3008
|
+ |
<p class="text-sm text-base-content/60">
|
|
3009
|
+ |
Open above, closed below. The trigger says the same thing either way, and it is
|
|
3010
|
+ |
the only part that changes when the stream ends: the pulsing "Thinking…" becomes
|
|
3011
|
+ |
a duration.
|
|
3012
|
+ |
</p>
|
|
3013
|
+ |
</div>
|
|
3014
|
+ |
"""
|
|
3015
|
+ |
end
|
|
3016
|
+ |
|
|
3017
|
+ |
defp component_demo(%{item: %{slug: "ai-chain-of-thought"}} = assigns) do
|
|
3018
|
+ |
~H"""
|
|
3019
|
+ |
<div class="space-y-3">
|
|
3020
|
+ |
<p class="text-sm text-base-content/60">
|
|
3021
|
+ |
Steps down a rail. <code>status</code>
|
|
3022
|
+ |
is the whole design: the step being worked reads at full strength, the ones behind
|
|
3023
|
+ |
it are muted, and the ones ahead are dimmed further, so the reader's eye lands on
|
|
3024
|
+ |
the present without an animation asking it to.
|
|
3025
|
+ |
</p>
|
|
3026
|
+ |
<Reasoning.chain_of_thought id="demo-cot" open>
|
|
3027
|
+ |
<Reasoning.chain_of_thought_header>
|
|
3028
|
+ |
Answering a question about this repository
|
|
3029
|
+ |
</Reasoning.chain_of_thought_header>
|
|
3030
|
+ |
<Reasoning.chain_of_thought_content>
|
|
3031
|
+ |
<Reasoning.chain_of_thought_step
|
|
3032
|
+ |
icon="search"
|
|
3033
|
+ |
label="Search for the admission path"
|
|
3034
|
+ |
description="Ranked by how often each file names a capability manifest."
|
|
3035
|
+ |
status={:complete}
|
|
3036
|
+ |
>
|
|
3037
|
+ |
<Reasoning.chain_of_thought_search_results>
|
|
3038
|
+ |
<Reasoning.chain_of_thought_search_result>
|
|
3039
|
+ |
lib/openagents/forge/targets.ex
|
|
3040
|
+ |
</Reasoning.chain_of_thought_search_result>
|
|
3041
|
+ |
<Reasoning.chain_of_thought_search_result>
|
|
3042
|
+ |
lib/openagents/cloud/placement.ex
|
|
3043
|
+ |
</Reasoning.chain_of_thought_search_result>
|
|
3044
|
+ |
<Reasoning.chain_of_thought_search_result>
|
|
3045
|
+ |
docs/cloud/INVARIANTS.md
|
|
3046
|
+ |
</Reasoning.chain_of_thought_search_result>
|
|
3047
|
+ |
</Reasoning.chain_of_thought_search_results>
|
|
3048
|
+ |
</Reasoning.chain_of_thought_step>
|
|
3049
|
+ |
<Reasoning.chain_of_thought_step
|
|
3050
|
+ |
icon="document"
|
|
3051
|
+ |
label="Read Placement.admit/2"
|
|
3052
|
+ |
description="The refusal is ahead of the scoring, not inside it."
|
|
3053
|
+ |
status={:complete}
|
|
3054
|
+ |
/>
|
|
3055
|
+ |
<Reasoning.chain_of_thought_step
|
|
3056
|
+ |
icon="chart"
|
|
3057
|
+ |
label="Check the last twenty runs for the same refusal"
|
|
3058
|
+ |
status={:active}
|
|
3059
|
+ |
>
|
|
3060
|
+ |
<Reasoning.chain_of_thought_image caption="Refusals by reason, last twenty runs">
|
|
3061
|
+ |
<img src={~p"/images/logo.svg"} alt="" class="h-24 w-auto opacity-60" />
|
|
3062
|
+ |
</Reasoning.chain_of_thought_image>
|
|
3063
|
+ |
</Reasoning.chain_of_thought_step>
|
|
3064
|
+ |
<Reasoning.chain_of_thought_step
|
|
3065
|
+ |
icon="edit-pencil"
|
|
3066
|
+ |
label="Write the answer"
|
|
3067
|
+ |
status={:pending}
|
|
3068
|
+ |
/>
|
|
3069
|
+ |
</Reasoning.chain_of_thought_content>
|
|
3070
|
+ |
</Reasoning.chain_of_thought>
|
|
3071
|
+ |
</div>
|
|
3072
|
+ |
"""
|
|
3073
|
+ |
end
|
|
3074
|
+ |
|
|
3075
|
+ |
defp component_demo(%{item: %{slug: "ai-tool"}} = assigns) do
|
|
3076
|
+ |
assigns = assign(assigns, :tool_calls, @demo_tool_calls)
|
|
3077
|
+ |
|
|
3078
|
+ |
~H"""
|
|
3079
|
+ |
<div class="space-y-4">
|
|
3080
|
+ |
<p class="text-sm text-base-content/60">
|
|
3081
|
+ |
Seven states, all of them here, because the badge is the taxonomy and one happy
|
|
3082
|
+ |
call documents none of it. Four describe a call's own progress — arguments still
|
|
3083
|
+ |
arriving, arguments complete, a result, a failure — and three describe a call that
|
|
3084
|
+ |
needed a person: waiting on approval, answered, and refused.
|
|
3085
|
+ |
</p>
|
|
3086
|
+ |
<Reasoning.tool :for={call <- @tool_calls} id={"demo-tool-#{call.state}"} open>
|
|
3087
|
+ |
<Reasoning.tool_header type={"tool-" <> call.name} tool_name={call.name} state={call.state} />
|
|
3088
|
+ |
<Reasoning.tool_content>
|
|
3089
|
+ |
<Reasoning.tool_input input={call.input} />
|
|
3090
|
+ |
<Reasoning.tool_output
|
|
3091
|
+ |
:if={call.output || call.error}
|
|
3092
|
+ |
output={call.output}
|
|
3093
|
+ |
error_text={call.error}
|
|
3094
|
+ |
/>
|
|
3095
|
+ |
</Reasoning.tool_content>
|
|
3096
|
+ |
</Reasoning.tool>
|
|
3097
|
+ |
<p class="text-sm text-base-content/60">
|
|
3098
|
+ |
Arguments and results are preformatted text, not highlighted code: the code-block
|
|
3099
|
+ |
port is <.link
|
|
3100
|
+ |
patch={~p"/components/ai-code-block"}
|
|
3101
|
+ |
class="underline underline-offset-2 hover:no-underline"
|
|
3102
|
+ |
>a separate component</.link>, and claiming syntax highlighting that is not
|
|
3103
|
+ |
happening would be worse than plain text.
|
|
3104
|
+ |
</p>
|
|
3105
|
+ |
</div>
|
|
3106
|
+ |
"""
|
|
3107
|
+ |
end
|
|
3108
|
+ |
|
|
3109
|
+ |
defp component_demo(%{item: %{slug: "ai-task"}} = assigns) do
|
|
3110
|
+ |
~H"""
|
|
3111
|
+ |
<div class="space-y-4">
|
|
3112
|
+ |
<p class="text-sm text-base-content/60">
|
|
3113
|
+ |
What one piece of work did, and which files it touched. Open by default, as
|
|
3114
|
+ |
upstream: a task is written into the transcript once it is finished, so the first
|
|
3115
|
+ |
thing a reader wants is its contents, not its title.
|
|
3116
|
+ |
</p>
|
|
3117
|
+ |
<Reasoning.task id="demo-task-open" open>
|
|
3118
|
+ |
<Reasoning.task_trigger title="Fixed the flaky targets test" />
|
|
3119
|
+ |
<Reasoning.task_content>
|
|
3120
|
+ |
<Reasoning.task_item>
|
|
3121
|
+ |
Read
|
|
3122
|
+ |
<Reasoning.task_item_file>
|
|
3123
|
+ |
test/openagents/forge/targets_test.exs
|
|
3124
|
+ |
</Reasoning.task_item_file>
|
|
3125
|
+ |
</Reasoning.task_item>
|
|
3126
|
+ |
<Reasoning.task_item>
|
|
3127
|
+ |
Found the ordering assumption in
|
|
3128
|
+ |
<Reasoning.task_item_file>lib/openagents/forge/targets.ex</Reasoning.task_item_file>
|
|
3129
|
+ |
</Reasoning.task_item>
|
|
3130
|
+ |
<Reasoning.task_item>
|
|
3131
|
+ |
Sorted the candidates by identifier before scoring
|
|
3132
|
+ |
</Reasoning.task_item>
|
|
3133
|
+ |
<Reasoning.task_item>
|
|
3134
|
+ |
Ran <code>mix test --seed 0</code>: 1416 passed, 14 excluded
|
|
3135
|
+ |
</Reasoning.task_item>
|
|
3136
|
+ |
</Reasoning.task_content>
|
|
3137
|
+ |
</Reasoning.task>
|
|
3138
|
+ |
<Reasoning.task id="demo-task-closed" open={false}>
|
|
3139
|
+ |
<Reasoning.task_trigger title="Checked the staging fleet before starting" />
|
|
3140
|
+ |
<Reasoning.task_content>
|
|
3141
|
+ |
<Reasoning.task_item>
|
|
3142
|
+ |
Read
|
|
3143
|
+ |
<Reasoning.task_item_file>infra/staging/outputs.tf</Reasoning.task_item_file>
|
|
3144
|
+ |
</Reasoning.task_item>
|
|
3145
|
+ |
</Reasoning.task_content>
|
|
3146
|
+ |
</Reasoning.task>
|
|
3147
|
+ |
<p class="text-sm text-base-content/60">
|
|
3148
|
+ |
The second one is closed, which is what a long run needs: everything the agent did
|
|
3149
|
+ |
stays in the transcript, and only the task under discussion stays open.
|
|
3150
|
+ |
</p>
|
|
3151
|
+ |
</div>
|
|
3152
|
+ |
"""
|
|
3153
|
+ |
end
|
|
3154
|
+ |
|
|
3155
|
+ |
defp component_demo(%{item: %{slug: "ai-plan"}} = assigns) do
|
|
3156
|
+ |
~H"""
|
|
3157
|
+ |
<div class="space-y-3">
|
|
3158
|
+ |
<p class="text-sm text-base-content/60">
|
|
3159
|
+ |
The one part of this batch that takes slots rather than siblings. A <code>details</code>
|
|
3160
|
+ |
hides every child but its summary, and this footer has to stay visible while the
|
|
3161
|
+ |
body collapses — approving a plan you cannot see is the one thing to prevent — so
|
|
3162
|
+ |
the parent places the footer outside the collapsing region.
|
|
3163
|
+ |
</p>
|
|
3164
|
+ |
<Reasoning.plan id="demo-plan" open>
|
|
3165
|
+ |
<:header>
|
|
3166
|
+ |
<div>
|
|
3167
|
+ |
<Reasoning.plan_title>Make the targets test deterministic</Reasoning.plan_title>
|
|
3168
|
+ |
<Reasoning.plan_description>
|
|
3169
|
+ |
Four steps, two of them in files this run has already read.
|
|
3170
|
+ |
</Reasoning.plan_description>
|
|
3171
|
+ |
</div>
|
|
3172
|
+ |
<Reasoning.plan_trigger />
|
|
3173
|
+ |
</:header>
|
|
3174
|
+ |
<Reasoning.task id="demo-plan-step-1" open={false}>
|
|
3175
|
+ |
<Reasoning.task_trigger title="Sort candidates by identifier before scoring" />
|
|
3176
|
+ |
<Reasoning.task_content>
|
|
3177
|
+ |
<Reasoning.task_item>
|
|
3178
|
+ |
<Reasoning.task_item_file>lib/openagents/forge/targets.ex</Reasoning.task_item_file>
|
|
3179
|
+ |
</Reasoning.task_item>
|
|
3180
|
+ |
</Reasoning.task_content>
|
|
3181
|
+ |
</Reasoning.task>
|
|
3182
|
+ |
<Reasoning.task id="demo-plan-step-2" open={false}>
|
|
3183
|
+ |
<Reasoning.task_trigger title="Assert the order, not just the membership" />
|
|
3184
|
+ |
<Reasoning.task_content>
|
|
3185
|
+ |
<Reasoning.task_item>
|
|
3186
|
+ |
<Reasoning.task_item_file>
|
|
3187
|
+ |
test/openagents/forge/targets_test.exs
|
|
3188
|
+ |
</Reasoning.task_item_file>
|
|
3189
|
+ |
</Reasoning.task_item>
|
|
3190
|
+ |
</Reasoning.task_content>
|
|
3191
|
+ |
</Reasoning.task>
|
|
3192
|
+ |
<Reasoning.task id="demo-plan-step-3" open={false}>
|
|
3193
|
+ |
<Reasoning.task_trigger title="Run the suite at three seeds" />
|
|
3194
|
+ |
<Reasoning.task_content>
|
|
3195
|
+ |
<Reasoning.task_item>mix test --seed 0, --seed 1, --seed 2</Reasoning.task_item>
|
|
3196
|
+ |
</Reasoning.task_content>
|
|
3197
|
+ |
</Reasoning.task>
|
|
3198
|
+ |
<:footer>
|
|
3199
|
+ |
<Reasoning.plan_action>
|
|
3200
|
+ |
<UI.button variant={:primary} size={:sm}>Approve</UI.button>
|
|
3201
|
+ |
</Reasoning.plan_action>
|
|
3202
|
+ |
<Reasoning.plan_action>
|
|
3203
|
+ |
<UI.button variant={:outline} size={:sm}>Ask for changes</UI.button>
|
|
3204
|
+ |
</Reasoning.plan_action>
|
|
3205
|
+ |
</:footer>
|
|
3206
|
+ |
</Reasoning.plan>
|
|
3207
|
+ |
<p class="text-sm text-base-content/60">
|
|
3208
|
+ |
Collapse it with the control in its header. The two actions stay where they are.
|
|
3209
|
+ |
</p>
|
|
3210
|
+ |
</div>
|
|
3211
|
+ |
"""
|
|
3212
|
+ |
end
|
|
3213
|
+ |
|
|
3214
|
+ |
defp component_demo(%{item: %{slug: "ai-checkpoint"}} = assigns) do
|
|
3215
|
+ |
~H"""
|
|
3216
|
+ |
<div class="space-y-3">
|
|
3217
|
+ |
<p class="text-sm text-base-content/60">
|
|
3218
|
+ |
A point the run can be returned to, drawn as a rule across the transcript so it
|
|
3219
|
+ |
reads as a division rather than another message. The rule fills whatever width the
|
|
3220
|
+ |
controls leave, which is why the label and the action can be any length.
|
|
3221
|
+ |
</p>
|
|
3222
|
+ |
<div class="space-y-4">
|
|
3223
|
+ |
<Reasoning.checkpoint id="demo-checkpoint-plain">
|
|
3224
|
+ |
<Reasoning.checkpoint_icon />
|
|
3225
|
+ |
<span class="px-2 text-xs">Before the rebase onto main</span>
|
|
3226
|
+ |
</Reasoning.checkpoint>
|
|
3227
|
+ |
<Reasoning.checkpoint id="demo-checkpoint-restore">
|
|
3228
|
+ |
<Reasoning.checkpoint_icon />
|
|
3229
|
+ |
<span class="px-2 text-xs">After attempt two</span>
|
|
3230
|
+ |
<Reasoning.checkpoint_trigger tooltip="Restore the worktree to this point">
|
|
3231
|
+ |
Restore
|
|
3232
|
+ |
</Reasoning.checkpoint_trigger>
|
|
3233
|
+ |
</Reasoning.checkpoint>
|
|
3234
|
+ |
<Reasoning.checkpoint id="demo-checkpoint-current">
|
|
3235
|
+ |
<Reasoning.checkpoint_icon name="saved-filled-xs" />
|
|
3236
|
+ |
<span class="px-2 text-xs text-foreground">Current</span>
|
|
3237
|
+ |
</Reasoning.checkpoint>
|
|
3238
|
+ |
</div>
|
|
3239
|
+ |
<p class="text-sm text-base-content/60">
|
|
3240
|
+ |
Upstream the restore control carries a Radix tooltip. There is no tooltip
|
|
3241
|
+ |
primitive here, so <code>tooltip</code>
|
|
3242
|
+ |
becomes the native <code>title</code>: same text, no script, still announced.
|
|
3243
|
+ |
</p>
|
|
3244
|
+ |
</div>
|
|
3245
|
+ |
"""
|
|
3246
|
+ |
end
|
|
3247
|
+ |
|
|
3248
|
+ |
# ── AI composer ───────────────────────────────────────────────────────────
|
|
3249
|
+ |
|
|
3250
|
+ |
defp component_demo(%{item: %{slug: "ai-prompt-input"}} = assigns) do
|
|
3251
|
+ |
~H"""
|
|
3252
|
+ |
<div class="space-y-4">
|
|
3253
|
+ |
<p class="text-sm text-base-content/60">
|
|
3254
|
+ |
A real Phoenix form around a real input group. The textarea is bound to a <code>Phoenix.HTML.FormField</code>, not to a loose string, so the composer submits
|
|
3255
|
+ |
the way every other form in this app does. Everything else — the growing height,
|
|
3256
|
+ |
Enter to submit, dropped and pasted files — is one colocated hook, because none of
|
|
3257
|
+ |
it can be expressed in markup.
|
|
3258
|
+ |
</p>
|
|
3259
|
+ |
<PromptInput.prompt_input
|
|
3260
|
+ |
id="demo-prompt-input"
|
|
3261
|
+ |
for={@composer_form}
|
|
3262
|
+ |
accept="image/*,text/*"
|
|
3263
|
+ |
phx-submit="save"
|
|
3264
|
+ |
>
|
|
3265
|
+ |
<:drop_overlay>Drop files to attach them</:drop_overlay>
|
|
3266
|
+ |
<PromptInput.prompt_input_body>
|
|
3267
|
+ |
<PromptInput.prompt_input_textarea
|
|
3268
|
+ |
field={@composer_form[:message]}
|
|
3269
|
+ |
placeholder="Ask about this repository, or start a run"
|
|
3270
|
+ |
/>
|
|
3271
|
+ |
</PromptInput.prompt_input_body>
|
|
3272
|
+ |
<PromptInput.prompt_input_toolbar>
|
|
3273
|
+ |
<PromptInput.prompt_input_tools>
|
|
3274
|
+ |
<PromptInput.prompt_input_action_menu_trigger menu="demo-prompt-input-actions" />
|
|
3275
|
+ |
<PromptInput.prompt_input_button tooltip="Search the web">
|
|
3276
|
+ |
<UI.icon name="globe" class="size-4" />
|
|
3277
|
+ |
</PromptInput.prompt_input_button>
|
|
3278
|
+ |
<PromptInput.prompt_input_model_select
|
|
3279
|
+ |
name="composer[model]"
|
|
3280
|
+ |
value="claude-opus-5"
|
|
3281
|
+ |
>
|
|
3282
|
+ |
<PromptInput.prompt_input_model_select_item value="claude-opus-5" selected>
|
|
3283
|
+ |
Claude Opus 5
|
|
3284
|
+ |
</PromptInput.prompt_input_model_select_item>
|
|
3285
|
+ |
<PromptInput.prompt_input_model_select_item value="claude-sonnet-4">
|
|
3286
|
+ |
Claude Sonnet 4
|
|
3287
|
+ |
</PromptInput.prompt_input_model_select_item>
|
|
3288
|
+ |
</PromptInput.prompt_input_model_select>
|
|
3289
|
+ |
</PromptInput.prompt_input_tools>
|
|
3290
|
+ |
<PromptInput.prompt_input_submit id="demo-prompt-input-submit" status={:ready} />
|
|
3291
|
+ |
</PromptInput.prompt_input_toolbar>
|
|
3292
|
+ |
</PromptInput.prompt_input>
|
|
3293
|
+ |
<PromptInput.prompt_input_action_menu id="demo-prompt-input-actions">
|
|
3294
|
+ |
<PromptInput.prompt_input_action_menu_content>
|
|
3295
|
+ |
<PromptInput.prompt_input_action_add_attachments for="demo-prompt-input" />
|
|
3296
|
+ |
<PromptInput.prompt_input_action_add_screenshot />
|
|
3297
|
+ |
</PromptInput.prompt_input_action_menu_content>
|
|
3298
|
+ |
</PromptInput.prompt_input_action_menu>
|
|
3299
|
+ |
|
|
3300
|
+ |
<p class="text-sm text-base-content/60">
|
|
3301
|
+ |
The submit control is four controls, not one control with four colours. Each
|
|
3302
|
+ |
status draws a different glyph and carries a different accessible name, and the
|
|
3303
|
+ |
streaming one is a stop button rather than a disabled send — which is the only
|
|
3304
|
+ |
state where the reader has something to do.
|
|
3305
|
+ |
</p>
|
|
3306
|
+ |
<div class="flex flex-wrap items-center gap-6">
|
|
3307
|
+ |
<div :for={status <- [:ready, :submitted, :streaming, :error]} class="space-y-2">
|
|
3308
|
+ |
<PromptInput.prompt_input_submit id={"demo-submit-#{status}"} status={status} />
|
|
3309
|
+ |
<p class="text-xs text-base-content/60">{status}</p>
|
|
3310
|
+ |
</div>
|
|
3311
|
+ |
</div>
|
|
3312
|
+ |
|
|
3313
|
+ |
<p class="text-sm text-base-content/60">
|
|
3314
|
+ |
With a header and a footer, the same group carries staged attachments above the
|
|
3315
|
+ |
text and a note below it, and the input group grows rather than scrolling.
|
|
3316
|
+ |
</p>
|
|
3317
|
+ |
<PromptInput.prompt_input id="demo-prompt-input-full" for={@composer_form} phx-submit="save">
|
|
3318
|
+ |
<PromptInput.prompt_input_header>
|
|
3319
|
+ |
<PromptInput.attachments variant={:inline}>
|
|
3320
|
+ |
<PromptInput.attachment variant={:inline}>
|
|
3321
|
+ |
<PromptInput.attachment_preview
|
|
3322
|
+ |
variant={:inline}
|
|
3323
|
+ |
media_category={:source}
|
|
3324
|
+ |
filename="targets.ex"
|
|
3325
|
+ |
/>
|
|
3326
|
+ |
<PromptInput.attachment_info variant={:inline} label="targets.ex" />
|
|
3327
|
+ |
<PromptInput.attachment_remove variant={:inline} label="Remove targets.ex" />
|
|
3328
|
+ |
</PromptInput.attachment>
|
|
3329
|
+ |
</PromptInput.attachments>
|
|
3330
|
+ |
</PromptInput.prompt_input_header>
|
|
3331
|
+ |
<PromptInput.prompt_input_body>
|
|
3332
|
+ |
<PromptInput.prompt_input_textarea
|
|
3333
|
+ |
id="demo-prompt-input-full-textarea"
|
|
3334
|
+ |
field={@composer_form[:message]}
|
|
3335
|
+ |
/>
|
|
3336
|
+ |
</PromptInput.prompt_input_body>
|
|
3337
|
+ |
<PromptInput.prompt_input_footer>
|
|
3338
|
+ |
This run can read the repository. It cannot push.
|
|
3339
|
+ |
</PromptInput.prompt_input_footer>
|
|
3340
|
+ |
<PromptInput.prompt_input_toolbar>
|
|
3341
|
+ |
<PromptInput.prompt_input_tools>
|
|
3342
|
+ |
<PromptInput.speech_input
|
|
3343
|
+ |
id="demo-prompt-input-speech"
|
|
3344
|
+ |
transcript_event="demo_transcript"
|
|
3345
|
+ |
/>
|
|
3346
|
+ |
</PromptInput.prompt_input_tools>
|
|
3347
|
+ |
<PromptInput.prompt_input_submit id="demo-prompt-input-full-submit" status={:streaming} />
|
|
3348
|
+ |
</PromptInput.prompt_input_toolbar>
|
|
3349
|
+ |
</PromptInput.prompt_input>
|
|
3350
|
+ |
</div>
|
|
3351
|
+ |
"""
|
|
3352
|
+ |
end
|
|
3353
|
+ |
|
|
3354
|
+ |
defp component_demo(%{item: %{slug: "ai-prompt-input-action-menu"}} = assigns) do
|
|
3355
|
+ |
~H"""
|
|
3356
|
+ |
<div class="space-y-3">
|
|
3357
|
+ |
<p class="text-sm text-base-content/60">
|
|
3358
|
+ |
Upstream this is a Radix dropdown. Here it is the native popover API: click out to
|
|
3359
|
+ |
dismiss,
|
|
3360
|
+ |
<UI.kbd>Esc</UI.kbd>
|
|
3361
|
+ |
to close, the trigger as the anchor, and no script. The two actions that ship with
|
|
3362
|
+ |
it are the two the composer always has — reach the file input, and capture the
|
|
3363
|
+ |
screen.
|
|
3364
|
+ |
</p>
|
|
3365
|
+ |
<div class="flex items-center gap-3">
|
|
3366
|
+ |
<PromptInput.prompt_input_action_menu_trigger menu="demo-action-menu" />
|
|
3367
|
+ |
<span class="text-sm text-base-content/60">Open the menu</span>
|
|
3368
|
+ |
</div>
|
|
3369
|
+ |
<PromptInput.prompt_input_action_menu id="demo-action-menu" label="Composer actions">
|
|
3370
|
+ |
<PromptInput.prompt_input_action_menu_content>
|
|
3371
|
+ |
<PromptInput.prompt_input_action_add_attachments for="demo-prompt-input" />
|
|
3372
|
+ |
<PromptInput.prompt_input_action_add_screenshot />
|
|
3373
|
+ |
<PromptInput.prompt_input_action_menu_item>
|
|
3374
|
+ |
<UI.icon name="folder" class="mr-2 size-4" /> Add a repository
|
|
3375
|
+ |
</PromptInput.prompt_input_action_menu_item>
|
|
3376
|
+ |
</PromptInput.prompt_input_action_menu_content>
|
|
3377
|
+ |
</PromptInput.prompt_input_action_menu>
|
|
3378
|
+ |
<p class="text-sm text-base-content/60">
|
|
3379
|
+ |
The attach action names the composer it belongs to rather than holding a reference
|
|
3380
|
+ |
to it, because the file input it clicks is rendered by that composer.
|
|
3381
|
+ |
</p>
|
|
3382
|
+ |
</div>
|
|
3383
|
+ |
"""
|
|
3384
|
+ |
end
|
|
3385
|
+ |
|
|
3386
|
+ |
defp component_demo(%{item: %{slug: "ai-prompt-input-model-select"}} = assigns) do
|
|
3387
|
+ |
~H"""
|
|
3388
|
+ |
<div class="space-y-3">
|
|
3389
|
+ |
<p class="text-sm text-base-content/60">
|
|
3390
|
+ |
A real <code>select</code>, which is the point: the model is part of what the
|
|
3391
|
+ |
composer submits, so it should arrive in the same params as the text rather than
|
|
3392
|
+ |
in client state the server has to be told about. The trigger is styled down to the
|
|
3393
|
+ |
toolbar's height so it sits in the row rather than on it.
|
|
3394
|
+ |
</p>
|
|
3395
|
+ |
<div class="flex flex-wrap items-center gap-4">
|
|
3396
|
+ |
<PromptInput.prompt_input_model_select name="demo[model]" value="claude-opus-5">
|
|
3397
|
+ |
<PromptInput.prompt_input_model_select_item value="claude-opus-5" selected>
|
|
3398
|
+ |
Claude Opus 5
|
|
3399
|
+ |
</PromptInput.prompt_input_model_select_item>
|
|
3400
|
+ |
<PromptInput.prompt_input_model_select_item value="claude-sonnet-4">
|
|
3401
|
+ |
Claude Sonnet 4
|
|
3402
|
+ |
</PromptInput.prompt_input_model_select_item>
|
|
3403
|
+ |
<PromptInput.prompt_input_model_select_item value="claude-haiku-4">
|
|
3404
|
+ |
Claude Haiku 4
|
|
3405
|
+ |
</PromptInput.prompt_input_model_select_item>
|
|
3406
|
+ |
</PromptInput.prompt_input_model_select>
|
|
3407
|
+ |
|
|
3408
|
+ |
<PromptInput.prompt_input_model_select
|
|
3409
|
+ |
id="demo-model-select-disabled"
|
|
3410
|
+ |
name="demo[locked_model]"
|
|
3411
|
+ |
value="claude-opus-5"
|
|
3412
|
+ |
label="Model, fixed by the run"
|
|
3413
|
+ |
disabled
|
|
3414
|
+ |
>
|
|
3415
|
+ |
<PromptInput.prompt_input_model_select_item value="claude-opus-5" selected>
|
|
3416
|
+ |
Claude Opus 5
|
|
3417
|
+ |
</PromptInput.prompt_input_model_select_item>
|
|
3418
|
+ |
</PromptInput.prompt_input_model_select>
|
|
3419
|
+ |
</div>
|
|
3420
|
+ |
<p class="text-sm text-base-content/60">
|
|
3421
|
+ |
The second one is fixed, which is what a resumed run needs: changing the model
|
|
3422
|
+ |
mid-run would make the transcript describe two different agents.
|
|
3423
|
+ |
</p>
|
|
3424
|
+ |
</div>
|
|
3425
|
+ |
"""
|
|
3426
|
+ |
end
|
|
3427
|
+ |
|
|
3428
|
+ |
defp component_demo(%{item: %{slug: "ai-attachments"}} = assigns) do
|
|
3429
|
+ |
~H"""
|
|
3430
|
+ |
<div class="space-y-6">
|
|
3431
|
+ |
<p class="text-sm text-base-content/60">
|
|
3432
|
+ |
Three layouts for the same file. <code>:grid</code>
|
|
3433
|
+ |
is the thumbnail wall above the composer, <code>:inline</code>
|
|
3434
|
+ |
is the chip row that fits inside it, and <code>:list</code>
|
|
3435
|
+ |
is the full-width row a review surface wants. The preview falls back to the glyph
|
|
3436
|
+ |
for the file's category, which is how an audio file stays distinguishable from a
|
|
3437
|
+ |
document with no thumbnail to show.
|
|
3438
|
+ |
</p>
|
|
3439
|
+ |
|
|
3440
|
+ |
<div class="space-y-2">
|
|
3441
|
+ |
<p class="text-xs uppercase tracking-wide text-base-content/60">Grid</p>
|
|
3442
|
+ |
<PromptInput.attachments id="demo-attachments-grid" variant={:grid}>
|
|
3443
|
+ |
<PromptInput.attachment variant={:grid}>
|
|
3444
|
+ |
<PromptInput.attachment_preview
|
|
3445
|
+ |
variant={:grid}
|
|
3446
|
+ |
media_category={:image}
|
|
3447
|
+ |
src={~p"/images/logo.svg"}
|
|
3448
|
+ |
filename="graph.svg"
|
|
3449
|
+ |
/>
|
|
3450
|
+ |
<PromptInput.attachment_remove variant={:grid} label="Remove graph.svg" />
|
|
3451
|
+ |
</PromptInput.attachment>
|
|
3452
|
+ |
<PromptInput.attachment variant={:grid}>
|
|
3453
|
+ |
<PromptInput.attachment_preview
|
|
3454
|
+ |
variant={:grid}
|
|
3455
|
+ |
media_category={:document}
|
|
3456
|
+ |
filename="receipt.pdf"
|
|
3457
|
+ |
/>
|
|
3458
|
+ |
<PromptInput.attachment_remove variant={:grid} label="Remove receipt.pdf" />
|
|
3459
|
+ |
</PromptInput.attachment>
|
|
3460
|
+ |
<PromptInput.attachment variant={:grid}>
|
|
3461
|
+ |
<PromptInput.attachment_preview
|
|
3462
|
+ |
variant={:grid}
|
|
3463
|
+ |
media_category={:audio}
|
|
3464
|
+ |
filename="standup.m4a"
|
|
3465
|
+ |
/>
|
|
3466
|
+ |
<PromptInput.attachment_remove variant={:grid} label="Remove standup.m4a" />
|
|
3467
|
+ |
</PromptInput.attachment>
|
|
3468
|
+ |
</PromptInput.attachments>
|
|
3469
|
+ |
</div>
|
|
3470
|
+ |
|
|
3471
|
+ |
<div class="space-y-2">
|
|
3472
|
+ |
<p class="text-xs uppercase tracking-wide text-base-content/60">Inline</p>
|
|
3473
|
+ |
<PromptInput.attachments id="demo-attachments-inline" variant={:inline}>
|
|
3474
|
+ |
<PromptInput.attachment variant={:inline}>
|
|
3475
|
+ |
<PromptInput.attachment_preview
|
|
3476
|
+ |
variant={:inline}
|
|
3477
|
+ |
media_category={:source}
|
|
3478
|
+ |
filename="targets.ex"
|
|
3479
|
+ |
/>
|
|
3480
|
+ |
<PromptInput.attachment_info variant={:inline} label="targets.ex" />
|
|
3481
|
+ |
<PromptInput.attachment_remove variant={:inline} label="Remove targets.ex" />
|
|
3482
|
+ |
</PromptInput.attachment>
|
|
3483
|
+ |
<PromptInput.attachment variant={:inline}>
|
|
3484
|
+ |
<PromptInput.attachment_preview
|
|
3485
|
+ |
variant={:inline}
|
|
3486
|
+ |
media_category={:video}
|
|
3487
|
+ |
filename="repro.mp4"
|
|
3488
|
+ |
/>
|
|
3489
|
+ |
<PromptInput.attachment_info variant={:inline} label="repro.mp4" />
|
|
3490
|
+ |
<PromptInput.attachment_remove variant={:inline} label="Remove repro.mp4" />
|
|
3491
|
+ |
</PromptInput.attachment>
|
|
3492
|
+ |
</PromptInput.attachments>
|
|
3493
|
+ |
</div>
|
|
3494
|
+ |
|
|
3495
|
+ |
<div class="space-y-2">
|
|
3496
|
+ |
<p class="text-xs uppercase tracking-wide text-base-content/60">List</p>
|
|
3497
|
+ |
<PromptInput.attachments id="demo-attachments-list" variant={:list}>
|
|
3498
|
+ |
<PromptInput.attachment variant={:list}>
|
|
3499
|
+ |
<PromptInput.attachment_preview
|
|
3500
|
+ |
variant={:list}
|
|
3501
|
+ |
media_category={:source}
|
|
3502
|
+ |
filename="placement.ex"
|
|
3503
|
+ |
/>
|
|
3504
|
+ |
<PromptInput.attachment_info
|
|
3505
|
+ |
variant={:list}
|
|
3506
|
+ |
label="lib/openagents/cloud/placement.ex"
|
|
3507
|
+ |
media_type="text/x-elixir"
|
|
3508
|
+ |
/>
|
|
3509
|
+ |
<PromptInput.attachment_remove variant={:list} label="Remove placement.ex" />
|
|
3510
|
+ |
</PromptInput.attachment>
|
|
3511
|
+ |
<PromptInput.attachment variant={:list}>
|
|
3512
|
+ |
<PromptInput.attachment_preview
|
|
3513
|
+ |
variant={:list}
|
|
3514
|
+ |
media_category={:unknown}
|
|
3515
|
+ |
filename="fleet.tfstate"
|
|
3516
|
+ |
/>
|
|
3517
|
+ |
<PromptInput.attachment_info
|
|
3518
|
+ |
variant={:list}
|
|
3519
|
+ |
label="infra/staging/fleet.tfstate"
|
|
3520
|
+ |
media_type="application/json"
|
|
3521
|
+ |
/>
|
|
3522
|
+ |
<PromptInput.attachment_remove variant={:list} label="Remove fleet.tfstate" />
|
|
3523
|
+ |
</PromptInput.attachment>
|
|
3524
|
+ |
</PromptInput.attachments>
|
|
3525
|
+ |
</div>
|
|
3526
|
+ |
|
|
3527
|
+ |
<div class="space-y-2">
|
|
3528
|
+ |
<p class="text-xs uppercase tracking-wide text-base-content/60">Empty</p>
|
|
3529
|
+ |
<PromptInput.attachment_empty id="demo-attachments-empty" />
|
|
3530
|
+ |
</div>
|
|
3531
|
+ |
</div>
|
|
3532
|
+ |
"""
|
|
3533
|
+ |
end
|
|
3534
|
+ |
|
|
3535
|
+ |
defp component_demo(%{item: %{slug: "ai-speech-input"}} = assigns) do
|
|
3536
|
+ |
~H"""
|
|
3537
|
+ |
<div class="space-y-3">
|
|
3538
|
+ |
<p class="text-sm text-base-content/60">
|
|
3539
|
+ |
Push-to-talk. The hook detects the Web Speech API, falls back to <code>MediaRecorder</code>, and disables itself when neither exists — then publishes
|
|
3540
|
+ |
what it found on the wrapper, so the whole treatment, including the three
|
|
3541
|
+ |
staggered rings while recording, is CSS keyed off data attributes rather than a
|
|
3542
|
+ |
re-render.
|
|
3543
|
+ |
</p>
|
|
3544
|
+ |
<div class="flex flex-wrap items-center gap-8">
|
|
3545
|
+ |
<PromptInput.speech_input id="demo-speech" transcript_event="demo_transcript" />
|
|
3546
|
+ |
<PromptInput.speech_input
|
|
3547
|
+ |
id="demo-speech-disabled"
|
|
3548
|
+ |
transcript_event="demo_transcript"
|
|
3549
|
+ |
disabled
|
|
3550
|
+ |
/>
|
|
3551
|
+ |
</div>
|
|
3552
|
+ |
<p class="text-sm text-base-content/60">
|
|
3553
|
+ |
Recognized text arrives as an ordinary LiveView event. Recorded audio cannot
|
|
3554
|
+ |
travel in one, so in the fallback the hook writes the blob onto a file input the
|
|
3555
|
+ |
caller names, and disables itself when there is no such input to write to.
|
|
3556
|
+ |
</p>
|
|
3557
|
+ |
</div>
|
|
3558
|
+ |
"""
|
|
3559
|
+ |
end
|
|
3560
|
+ |
|
|
3561
|
+ |
defp component_demo(%{item: %{slug: "ai-mic-selector"}} = assigns) do
|
|
3562
|
+ |
~H"""
|
|
3563
|
+ |
<div class="space-y-3">
|
|
3564
|
+ |
<p class="text-sm text-base-content/60">
|
|
3565
|
+ |
A native <code>select</code>
|
|
3566
|
+ |
the browser fills in after mount, which is why it carries <code>phx-update="ignore"</code>: the device list is knowledge the client has and
|
|
3567
|
+ |
the server does not, and a re-render must not throw it away. Until permission is
|
|
3568
|
+ |
granted the list is the placeholder alone, which is honest — the browser refuses
|
|
3569
|
+ |
to name devices it has not been allowed to.
|
|
3570
|
+ |
</p>
|
|
3571
|
+ |
<div class="flex flex-wrap items-center gap-4">
|
|
3572
|
+ |
<PromptInput.mic_selector id="demo-mic-selector" name="demo[mic]" />
|
|
3573
|
+ |
<PromptInput.mic_selector id="demo-mic-selector-seeded" name="demo[seeded_mic]">
|
|
3574
|
+ |
<PromptInput.mic_selector_item value="default" selected>
|
|
3575
|
+ |
MacBook Pro Microphone
|
|
3576
|
+ |
</PromptInput.mic_selector_item>
|
|
3577
|
+ |
<PromptInput.mic_selector_item value="usb-1">
|
|
3578
|
+ |
Shure MV7 (14ed:1012)
|
|
3579
|
+ |
</PromptInput.mic_selector_item>
|
|
3580
|
+ |
</PromptInput.mic_selector>
|
|
3581
|
+ |
</div>
|
|
3582
|
+ |
<p class="text-sm text-base-content/60">
|
|
3583
|
+ |
The second one is seeded from the server so the shape is visible here. In use, the
|
|
3584
|
+ |
hook replaces whatever it finds.
|
|
3585
|
+ |
</p>
|
|
3586
|
+ |
</div>
|
|
3587
|
+ |
"""
|
|
3588
|
+ |
end
|
|
3589
|
+ |
|
|
3590
|
+ |
defp component_demo(%{item: %{slug: "ai-model-selector"}} = assigns) do
|
|
3591
|
+ |
~H"""
|
|
3592
|
+ |
<div class="space-y-3">
|
|
3593
|
+ |
<p class="text-sm text-base-content/60">
|
|
3594
|
+ |
The composer's select is for two models. This is for thirty: a native popover
|
|
3595
|
+ |
holding a search field, grouped rows, and shortcuts. Filtering runs in the browser
|
|
3596
|
+ |
against text already on the page, so typing costs no round trip and an empty
|
|
3597
|
+ |
result has words rather than a blank panel.
|
|
3598
|
+ |
</p>
|
|
3599
|
+ |
<div class="flex items-center gap-3">
|
|
3600
|
+ |
<PromptInput.model_selector_trigger panel="demo-model-selector">
|
|
3601
|
+ |
<PromptInput.model_selector_logo src={~p"/images/logo.svg"} provider="OpenAgents" />
|
|
3602
|
+ |
<PromptInput.model_selector_name>Claude Opus 5</PromptInput.model_selector_name>
|
|
3603
|
+ |
</PromptInput.model_selector_trigger>
|
|
3604
|
+ |
<span class="text-sm text-base-content/60">Open, then type to filter</span>
|
|
3605
|
+ |
</div>
|
|
3606
|
+ |
<PromptInput.model_selector id="demo-model-selector" label="Select a model">
|
|
3607
|
+ |
<PromptInput.model_selector_input placeholder="Search models..." />
|
|
3608
|
+ |
<PromptInput.model_selector_list>
|
|
3609
|
+ |
<PromptInput.model_selector_group heading="Anthropic">
|
|
3610
|
+ |
<PromptInput.model_selector_item value="Claude Opus 5" selected>
|
|
3611
|
+ |
<PromptInput.model_selector_name>Claude Opus 5</PromptInput.model_selector_name>
|
|
3612
|
+ |
<PromptInput.model_selector_shortcut>⌘1</PromptInput.model_selector_shortcut>
|
|
3613
|
+ |
</PromptInput.model_selector_item>
|
|
3614
|
+ |
<PromptInput.model_selector_item value="Claude Sonnet 4">
|
|
3615
|
+ |
<PromptInput.model_selector_name>Claude Sonnet 4</PromptInput.model_selector_name>
|
|
3616
|
+ |
<PromptInput.model_selector_shortcut>⌘2</PromptInput.model_selector_shortcut>
|
|
3617
|
+ |
</PromptInput.model_selector_item>
|
|
3618
|
+ |
<PromptInput.model_selector_item value="Claude Haiku 4">
|
|
3619
|
+ |
<PromptInput.model_selector_name>Claude Haiku 4</PromptInput.model_selector_name>
|
|
3620
|
+ |
</PromptInput.model_selector_item>
|
|
3621
|
+ |
</PromptInput.model_selector_group>
|
|
3622
|
+ |
<PromptInput.model_selector_separator />
|
|
3623
|
+ |
<PromptInput.model_selector_group heading="On this fleet">
|
|
3624
|
+ |
<PromptInput.model_selector_item value="Psion 1B pretrained">
|
|
3625
|
+ |
<PromptInput.model_selector_name>Psion 1B</PromptInput.model_selector_name>
|
|
3626
|
+ |
</PromptInput.model_selector_item>
|
|
3627
|
+ |
</PromptInput.model_selector_group>
|
|
3628
|
+ |
<PromptInput.model_selector_empty />
|
|
3629
|
+ |
</PromptInput.model_selector_list>
|
|
3630
|
+ |
</PromptInput.model_selector>
|
|
3631
|
+ |
<div class="flex items-center gap-3">
|
|
3632
|
+ |
<span class="text-sm text-base-content/60">Several providers behind one row:</span>
|
|
3633
|
+ |
<PromptInput.model_selector_logo_group>
|
|
3634
|
+ |
<PromptInput.model_selector_logo src={~p"/images/logo.svg"} provider="OpenAgents" />
|
|
3635
|
+ |
<PromptInput.model_selector_logo src={~p"/images/logo.svg"} provider="Psionic" />
|
|
3636
|
+ |
</PromptInput.model_selector_logo_group>
|
|
3637
|
+ |
</div>
|
|
3638
|
+ |
</div>
|
|
3639
|
+ |
"""
|
|
3640
|
+ |
end
|
|
3641
|
+ |
|
|
3642
|
+ |
defp component_demo(%{item: %{slug: "ai-queue"}} = assigns) do
|
|
3643
|
+ |
~H"""
|
|
3644
|
+ |
<div class="space-y-3">
|
|
3645
|
+ |
<p class="text-sm text-base-content/60">
|
|
3646
|
+ |
Turns the reader has already written but not yet sent. Each section is a native <code>details</code>, so the chevron rotates off the element's own
|
|
3647
|
+ |
<code>open</code>
|
|
3648
|
+ |
attribute and the disclosure works before any JavaScript loads.
|
|
3649
|
+ |
</p>
|
|
3650
|
+ |
<PromptInput.queue id="demo-queue">
|
|
3651
|
+ |
<PromptInput.queue_section open>
|
|
3652
|
+ |
<PromptInput.queue_section_trigger>
|
|
3653
|
+ |
<PromptInput.queue_section_label label="queued" count={2}>
|
|
3654
|
+ |
<:icon><UI.icon name="clock" class="size-4" /></:icon>
|
|
3655
|
+ |
</PromptInput.queue_section_label>
|
|
3656
|
+ |
</PromptInput.queue_section_trigger>
|
|
3657
|
+ |
<PromptInput.queue_section_content>
|
|
3658
|
+ |
<PromptInput.queue_list>
|
|
3659
|
+ |
<PromptInput.queue_item>
|
|
3660
|
+ |
<PromptInput.queue_item_indicator />
|
|
3661
|
+ |
<PromptInput.queue_item_content>
|
|
3662
|
+ |
Run the suite at three seeds and report the flaky ones
|
|
3663
|
+ |
<PromptInput.queue_item_description>
|
|
3664
|
+ |
Sends after the current turn
|
|
3665
|
+ |
</PromptInput.queue_item_description>
|
|
3666
|
+ |
</PromptInput.queue_item_content>
|
|
3667
|
+ |
<PromptInput.queue_item_actions>
|
|
3668
|
+ |
<PromptInput.queue_item_action label="Remove from the queue">
|
|
3669
|
+ |
<UI.icon name="x" class="size-4" />
|
|
3670
|
+ |
</PromptInput.queue_item_action>
|
|
3671
|
+ |
</PromptInput.queue_item_actions>
|
|
3672
|
+ |
</PromptInput.queue_item>
|
|
3673
|
+ |
<PromptInput.queue_item>
|
|
3674
|
+ |
<PromptInput.queue_item_indicator />
|
|
3675
|
+ |
<PromptInput.queue_item_content>
|
|
3676
|
+ |
Open an issue for whatever stays red
|
|
3677
|
+ |
<PromptInput.queue_item_attachment>
|
|
3678
|
+ |
<PromptInput.queue_item_file>targets_test.exs</PromptInput.queue_item_file>
|
|
3679
|
+ |
<PromptInput.queue_item_image src={~p"/images/logo.svg"} alt="" />
|
|
3680
|
+ |
</PromptInput.queue_item_attachment>
|
|
3681
|
+ |
</PromptInput.queue_item_content>
|
|
3682
|
+ |
</PromptInput.queue_item>
|
|
3683
|
+ |
</PromptInput.queue_list>
|
|
3684
|
+ |
</PromptInput.queue_section_content>
|
|
3685
|
+ |
</PromptInput.queue_section>
|
|
3686
|
+ |
|
|
3687
|
+ |
<PromptInput.queue_section open={false}>
|
|
3688
|
+ |
<PromptInput.queue_section_trigger>
|
|
3689
|
+ |
<PromptInput.queue_section_label label="sent" count={1}>
|
|
3690
|
+ |
<:icon><UI.icon name="check" class="size-4" /></:icon>
|
|
3691
|
+ |
</PromptInput.queue_section_label>
|
|
3692
|
+ |
</PromptInput.queue_section_trigger>
|
|
3693
|
+ |
<PromptInput.queue_section_content>
|
|
3694
|
+ |
<PromptInput.queue_list>
|
|
3695
|
+ |
<PromptInput.queue_item>
|
|
3696
|
+ |
<PromptInput.queue_item_indicator completed />
|
|
3697
|
+ |
<PromptInput.queue_item_content completed>
|
|
3698
|
+ |
Which module decides whether an SCV run is admitted?
|
|
3699
|
+ |
</PromptInput.queue_item_content>
|
|
3700
|
+ |
</PromptInput.queue_item>
|
|
3701
|
+ |
</PromptInput.queue_list>
|
|
3702
|
+ |
</PromptInput.queue_section_content>
|
|
3703
|
+ |
</PromptInput.queue_section>
|
|
3704
|
+ |
</PromptInput.queue>
|
|
3705
|
+ |
|
|
3706
|
+ |
<p class="text-sm text-base-content/60">With nothing waiting:</p>
|
|
3707
|
+ |
<PromptInput.queue id="demo-queue-empty">
|
|
3708
|
+ |
<PromptInput.queue_empty />
|
|
3709
|
+ |
</PromptInput.queue>
|
|
3710
|
+ |
</div>
|
|
3711
|
+ |
"""
|
|
3712
|
+ |
end
|
|
3713
|
+ |
|
|
3714
|
+ |
# ── AI evidence ───────────────────────────────────────────────────────────
|
|
3715
|
+ |
|
|
3716
|
+ |
defp component_demo(%{item: %{slug: "ai-code-block"}} = assigns) do
|
|
3717
|
+ |
assigns = assign(assigns, :code, @demo_code)
|
|
3718
|
+ |
|
|
3719
|
+ |
~H"""
|
|
3720
|
+ |
<div class="space-y-4">
|
|
3721
|
+ |
<p class="text-sm text-base-content/60">
|
|
3722
|
+ |
Chrome, line numbers, and a copy affordance, but no syntax highlighting: the
|
|
3723
|
+ |
source tokenizes with Shiki in the browser, which is a second rendering engine
|
|
3724
|
+ |
this page has not earned. The numbers come from a CSS counter, so selecting the
|
|
3725
|
+ |
block copies the code without them.
|
|
3726
|
+ |
</p>
|
|
3727
|
+ |
<Evidence.code_block
|
|
3728
|
+ |
id="demo-code-block"
|
|
3729
|
+ |
code={@code}
|
|
3730
|
+ |
language="elixir"
|
|
3731
|
+ |
filename="lib/openagents/cloud/placement.ex"
|
|
3732
|
+ |
show_line_numbers
|
|
3733
|
+ |
>
|
|
3734
|
+ |
<:actions>
|
|
3735
|
+ |
<UI.text_button>Open in the repository</UI.text_button>
|
|
3736
|
+ |
</:actions>
|
|
3737
|
+ |
</Evidence.code_block>
|
|
3738
|
+ |
<p class="text-sm text-base-content/60">
|
|
3739
|
+ |
Without a filename, a language, or actions, the header holds only the copy
|
|
3740
|
+ |
control, and the numbers can be left off for a fragment nobody is going to cite by
|
|
3741
|
+ |
line.
|
|
3742
|
+ |
</p>
|
|
3743
|
+ |
<Evidence.code_block id="demo-code-block-bare" code={@code} />
|
|
3744
|
+ |
</div>
|
|
3745
|
+ |
"""
|
|
3746
|
+ |
end
|
|
3747
|
+ |
|
|
3748
|
+ |
defp component_demo(%{item: %{slug: "ai-snippet"}} = assigns) do
|
|
3749
|
+ |
~H"""
|
|
3750
|
+ |
<div class="space-y-3">
|
|
3751
|
+ |
<p class="text-sm text-base-content/60">
|
|
3752
|
+ |
One command in a read-only field rather than in a paragraph, so a reader can
|
|
3753
|
+ |
select it, tab to it, and copy it without selecting the prose around it. The
|
|
3754
|
+ |
prefix is decorative: it says "this is a shell command" without becoming part of
|
|
3755
|
+ |
what gets copied.
|
|
3756
|
+ |
</p>
|
|
3757
|
+ |
<div class="space-y-3">
|
|
3758
|
+ |
<Evidence.snippet id="demo-snippet-shell" code="mix precommit" prefix="$" />
|
|
3759
|
+ |
<Evidence.snippet
|
|
3760
|
+ |
id="demo-snippet-long"
|
|
3761
|
+ |
code="git fetch openagents && git rebase openagents/main && git push openagents HEAD:main"
|
|
3762
|
+ |
prefix="$"
|
|
3763
|
+ |
label="Push to the forge"
|
|
3764
|
+ |
/>
|
|
3765
|
+ |
<Evidence.snippet
|
|
3766
|
+ |
id="demo-snippet-plain"
|
|
3767
|
+ |
code="OPENAGENTS_RELEASE_VSN=0.3.0"
|
|
3768
|
+ |
copy={false}
|
|
3769
|
+ |
label="Environment variable"
|
|
3770
|
+ |
/>
|
|
3771
|
+ |
</div>
|
|
3772
|
+ |
</div>
|
|
3773
|
+ |
"""
|
|
3774
|
+ |
end
|
|
3775
|
+ |
|
|
3776
|
+ |
defp component_demo(%{item: %{slug: "ai-terminal"}} = assigns) do
|
|
3777
|
+ |
assigns = assign(assigns, :output, @demo_terminal_output)
|
|
3778
|
+ |
|
|
3779
|
+ |
~H"""
|
|
3780
|
+ |
<div class="space-y-4">
|
|
3781
|
+ |
<p class="text-sm text-base-content/60">
|
|
3782
|
+ |
A fixed dark ground rather than a themed one, because terminal output means the
|
|
3783
|
+ |
colours the program chose, and repainting them by palette would make the same run
|
|
3784
|
+ |
look like two different runs. Escape sequences are not parsed: strip them before
|
|
3785
|
+ |
passing the output, or the reader sees them.
|
|
3786
|
+ |
</p>
|
|
3787
|
+ |
<Evidence.terminal
|
|
3788
|
+ |
id="demo-terminal-streaming"
|
|
3789
|
+ |
title="scv-13 — mix test"
|
|
3790
|
+ |
output={@output}
|
|
3791
|
+ |
status="Running"
|
|
3792
|
+ |
streaming
|
|
3793
|
+ |
>
|
|
3794
|
+ |
<:actions>
|
|
3795
|
+ |
<UI.text_button>Stop</UI.text_button>
|
|
3796
|
+ |
</:actions>
|
|
3797
|
+ |
</Evidence.terminal>
|
|
3798
|
+ |
<p class="text-sm text-base-content/60">
|
|
3799
|
+ |
Composed by hand instead, one line at a time, when the prompt matters as much as
|
|
3800
|
+ |
the output.
|
|
3801
|
+ |
</p>
|
|
3802
|
+ |
<Evidence.terminal id="demo-terminal-lines" title="scv-13 — worktree">
|
|
3803
|
+ |
<Evidence.terminal_line>git status --short</Evidence.terminal_line>
|
|
3804
|
+ |
<Evidence.terminal_line prompt=" ">M lib/openagents/forge/targets.ex</Evidence.terminal_line>
|
|
3805
|
+ |
<Evidence.terminal_line prompt=" ">
|
|
3806
|
+ |
M test/openagents/forge/targets_test.exs
|
|
3807
|
+ |
</Evidence.terminal_line>
|
|
3808
|
+ |
<Evidence.terminal_line>git stash list</Evidence.terminal_line>
|
|
3809
|
+ |
</Evidence.terminal>
|
|
3810
|
+ |
</div>
|
|
3811
|
+ |
"""
|
|
3812
|
+ |
end
|
|
3813
|
+ |
|
|
3814
|
+ |
defp component_demo(%{item: %{slug: "ai-sources"}} = assigns) do
|
|
3815
|
+ |
~H"""
|
|
3816
|
+ |
<div class="space-y-3">
|
|
3817
|
+ |
<p class="text-sm text-base-content/60">
|
|
3818
|
+ |
The count comes first and the list only on request, which is the right default for
|
|
3819
|
+ |
something a reader checks rather than reads: they want to know an answer was
|
|
3820
|
+ |
grounded before they want to know in what. A native <code>details</code>, so it needs no script.
|
|
3821
|
+ |
</p>
|
|
3822
|
+ |
<Evidence.sources id="demo-sources" count={4}>
|
|
3823
|
+ |
<Evidence.source
|
|
3824
|
+ |
href="https://openagents.com/OpenAgentsInc/openagents.com"
|
|
3825
|
+ |
title="lib/openagents/cloud/placement.ex"
|
|
3826
|
+ |
/>
|
|
3827
|
+ |
<Evidence.source
|
|
3828
|
+ |
href="https://openagents.com/OpenAgentsInc/openagents.com"
|
|
3829
|
+ |
title="docs/cloud/INVARIANTS.md"
|
|
3830
|
+ |
/>
|
|
3831
|
+ |
<Evidence.source href="https://hexdocs.pm/phoenix_live_view" title="Phoenix.LiveView" />
|
|
3832
|
+ |
<Evidence.source href="https://openagents.com/OpenAgentsInc/openagents.com" />
|
|
3833
|
+ |
</Evidence.sources>
|
|
3834
|
+ |
<p class="text-sm text-base-content/60">
|
|
3835
|
+ |
The last one has no title, so it falls back to the address. A source that cannot
|
|
3836
|
+ |
be named is still a source, and hiding it would overstate how much of the answer
|
|
3837
|
+ |
is accounted for.
|
|
3838
|
+ |
</p>
|
|
3839
|
+ |
<Evidence.sources id="demo-sources-open" count={1} open>
|
|
3840
|
+ |
<Evidence.source
|
|
3841
|
+ |
href="https://openagents.com/OpenAgentsInc/openagents.com"
|
|
3842
|
+ |
title="AGENTS.md"
|
|
3843
|
+ |
/>
|
|
3844
|
+ |
</Evidence.sources>
|
|
3845
|
+ |
</div>
|
|
3846
|
+ |
"""
|
|
3847
|
+ |
end
|
|
3848
|
+ |
|
|
3849
|
+ |
defp component_demo(%{item: %{slug: "ai-inline-citation"}} = assigns) do
|
|
3850
|
+ |
~H"""
|
|
3851
|
+ |
<div class="space-y-3">
|
|
3852
|
+ |
<p class="text-sm text-base-content/60">
|
|
3853
|
+ |
A hostname chip mid-sentence, so the reader can see what a claim rests on without
|
|
3854
|
+ |
leaving the line. The card opens on hover and on focus within, which is what makes
|
|
3855
|
+ |
it reachable from the keyboard; the source paginates its sources in a carousel,
|
|
3856
|
+ |
and they are simply listed here, because a citation is scoped to one claim and the
|
|
3857
|
+ |
count stays small.
|
|
3858
|
+ |
</p>
|
|
3859
|
+ |
<p class="max-w-[68ch] text-sm leading-relaxed">
|
|
3860
|
+ |
An expired capability manifest is refused before scoring <Evidence.inline_citation id="demo-citation">
|
|
3861
|
+ |
rather than ranked last
|
|
3862
|
+ |
<:source
|
|
3863
|
+ |
url="https://openagents.com/OpenAgentsInc/openagents.com/blob/main/docs/cloud/INVARIANTS.md"
|
|
3864
|
+ |
title="Cloud invariants"
|
|
3865
|
+ |
description="Admission checks capability freshness ahead of placement."
|
|
3866
|
+ |
/>
|
|
3867
|
+ |
<:source
|
|
3868
|
+ |
url="https://openagents.com/OpenAgentsInc/openagents.com/blob/main/lib/openagents/cloud/placement.ex"
|
|
3869
|
+ |
title="Placement.admit/2"
|
|
3870
|
+ |
/>
|
|
3871
|
+ |
</Evidence.inline_citation>, so it never reaches the queue at all.
|
|
3872
|
+ |
</p>
|
|
3873
|
+ |
<p class="max-w-[68ch] text-sm leading-relaxed">
|
|
3874
|
+ |
The quote treatment carries the sentence a source actually said: <Evidence.inline_citation id="demo-citation-quote">
|
|
3875
|
+ |
the refusal is a policy decision
|
|
3876
|
+ |
<:source url="https://openagents.com/OpenAgentsInc/openagents.com" title="AGENTS.md" />
|
|
3877
|
+ |
</Evidence.inline_citation>.
|
|
3878
|
+ |
</p>
|
|
3879
|
+ |
<Evidence.inline_citation_quote>
|
|
3880
|
+ |
Treat invariant changes as policy changes.
|
|
3881
|
+ |
</Evidence.inline_citation_quote>
|
|
3882
|
+ |
</div>
|
|
3883
|
+ |
"""
|
|
3884
|
+ |
end
|
|
3885
|
+ |
|
|
3886
|
+ |
defp component_demo(%{item: %{slug: "ai-context"}} = assigns) do
|
|
3887
|
+ |
~H"""
|
|
3888
|
+ |
<div class="space-y-3">
|
|
3889
|
+ |
<p class="text-sm text-base-content/60">
|
|
3890
|
+ |
How much of the window a turn spent, said twice: as a percentage and as an arc, so
|
|
3891
|
+ |
the meter survives greyscale and a screen reader alike. The ring is a masked conic
|
|
3892
|
+ |
gradient rather than a drawn arc, because inline SVG is not allowed in a product
|
|
3893
|
+ |
surface here. Costs are attributes — there is no pricing table in this app, so a
|
|
3894
|
+ |
caller that knows the price passes it.
|
|
3895
|
+ |
</p>
|
|
3896
|
+ |
<div class="flex flex-wrap items-center gap-6">
|
|
3897
|
+ |
<Evidence.context
|
|
3898
|
+ |
id="demo-context"
|
|
3899
|
+ |
used_tokens={128_400}
|
|
3900
|
+ |
max_tokens={1_000_000}
|
|
3901
|
+ |
input_tokens={96_000}
|
|
3902
|
+ |
output_tokens={12_400}
|
|
3903
|
+ |
reasoning_tokens={14_000}
|
|
3904
|
+ |
cached_tokens={6_000}
|
|
3905
|
+ |
input_cost={0.29}
|
|
3906
|
+ |
output_cost={0.19}
|
|
3907
|
+ |
reasoning_cost={0.21}
|
|
3908
|
+ |
cached_cost={0.01}
|
|
3909
|
+ |
total_cost={0.70}
|
|
3910
|
+ |
/>
|
|
3911
|
+ |
<Evidence.context id="demo-context-high" used_tokens={870_000} max_tokens={1_000_000} />
|
|
3912
|
+ |
<Evidence.context id="demo-context-over" used_tokens={1_120_000} max_tokens={1_000_000} />
|
|
3913
|
+ |
</div>
|
|
3914
|
+ |
<p class="text-sm text-base-content/60">
|
|
3915
|
+ |
The third one is over budget. A bar that silently pins at full hides the one state
|
|
3916
|
+ |
worth seeing, so it clamps its width, turns to the danger tone, and says so in an
|
|
3917
|
+ |
attribute a test can assert on.
|
|
3918
|
+ |
</p>
|
|
3919
|
+ |
</div>
|
|
3920
|
+ |
"""
|
|
3921
|
+ |
end
|
|
3922
|
+ |
|
|
3923
|
+ |
defp component_demo(%{item: %{slug: "ai-artifact"}} = assigns) do
|
|
3924
|
+ |
assigns = assign(assigns, :code, @demo_code)
|
|
3925
|
+ |
|
|
3926
|
+ |
~H"""
|
|
3927
|
+ |
<div class="space-y-3">
|
|
3928
|
+ |
<p class="text-sm text-base-content/60">
|
|
3929
|
+ |
Something the model produced, framed so the actions that act on it sit beside its
|
|
3930
|
+ |
name rather than under its contents. The body is whatever the artifact is — a
|
|
3931
|
+ |
patch, a document, a chart — and scrolls on its own, so a long artifact does not
|
|
3932
|
+ |
push the rest of the turn off the page.
|
|
3933
|
+ |
</p>
|
|
3934
|
+ |
<Evidence.artifact
|
|
3935
|
+ |
id="demo-artifact"
|
|
3936
|
+ |
title="targets.ex"
|
|
3937
|
+ |
description="Sorts candidates by identifier before scoring"
|
|
3938
|
+ |
class="h-72"
|
|
3939
|
+ |
>
|
|
3940
|
+ |
<:actions>
|
|
3941
|
+ |
<Evidence.artifact_action icon="copy" label="Copy the patch" />
|
|
3942
|
+ |
<Evidence.artifact_action icon="download" label="Download the patch" />
|
|
3943
|
+ |
<Evidence.artifact_action
|
|
3944
|
+ |
icon="external-link"
|
|
3945
|
+ |
label="Open in the repository"
|
|
3946
|
+ |
tooltip="Open lib/openagents/forge/targets.ex"
|
|
3947
|
+ |
/>
|
|
3948
|
+ |
</:actions>
|
|
3949
|
+ |
<Evidence.code_block id="demo-artifact-code" code={@code} language="elixir" copy={false} />
|
|
3950
|
+ |
</Evidence.artifact>
|
|
3951
|
+ |
</div>
|
|
3952
|
+ |
"""
|
|
3953
|
+ |
end
|
|
3954
|
+ |
|
|
3955
|
+ |
defp component_demo(%{item: %{slug: "ai-confirmation"}} = assigns) do
|
|
3956
|
+ |
~H"""
|
|
3957
|
+ |
<div class="space-y-4">
|
|
3958
|
+ |
<p class="text-sm text-base-content/60">
|
|
3959
|
+ |
An ask before a step that cannot be taken back, and — the part that matters — it
|
|
3960
|
+ |
keeps showing its answer afterwards. A confirmation that vanishes once decided
|
|
3961
|
+ |
leaves a transcript that cannot say who allowed what, which is exactly what a
|
|
3962
|
+ |
transcript is for.
|
|
3963
|
+ |
</p>
|
|
3964
|
+ |
<Evidence.confirmation
|
|
3965
|
+ |
id="demo-confirmation-requested"
|
|
3966
|
+ |
state={:requested}
|
|
3967
|
+ |
title="Push scv-13's branch to the forge and open a pull request?"
|
|
3968
|
+ |
>
|
|
3969
|
+ |
<:actions>
|
|
3970
|
+ |
<Evidence.confirmation_action variant={:primary}>Approve</Evidence.confirmation_action>
|
|
3971
|
+ |
<Evidence.confirmation_action variant={:outline}>Deny</Evidence.confirmation_action>
|
|
3972
|
+ |
</:actions>
|
|
3973
|
+ |
</Evidence.confirmation>
|
|
3974
|
+ |
<Evidence.confirmation
|
|
3975
|
+ |
id="demo-confirmation-approved"
|
|
3976
|
+ |
state={:approved}
|
|
3977
|
+ |
title="Push scv-13's branch to the forge and open a pull request?"
|
|
3978
|
+ |
reason="Approved by mason. The suite was green at three seeds."
|
|
3979
|
+ |
/>
|
|
3980
|
+ |
<Evidence.confirmation
|
|
3981
|
+ |
id="demo-confirmation-denied"
|
|
3982
|
+ |
state={:denied}
|
|
3983
|
+ |
title="Force-push over the staging branch?"
|
|
3984
|
+ |
reason="Denied by mason. Another run owns that branch."
|
|
3985
|
+ |
/>
|
|
3986
|
+ |
<p class="text-sm text-base-content/60">
|
|
3987
|
+ |
The decided ones carry no controls, because there is nothing left to decide, and
|
|
3988
|
+ |
the reason is where the transcript earns its keep.
|
|
3989
|
+ |
</p>
|
|
3990
|
+ |
</div>
|
|
3991
|
+ |
"""
|
|
3992
|
+ |
end
|
|
3993
|
+ |
|
|
3994
|
+ |
defp component_demo(%{item: %{slug: "ai-question"}} = assigns) do
|
|
3995
|
+ |
~H"""
|
|
3996
|
+ |
<div class="space-y-4">
|
|
3997
|
+ |
<p class="text-sm text-base-content/60">
|
|
3998
|
+ |
Both halves matter. A model that can only offer choices asks the wrong question
|
|
3999
|
+ |
sooner or later, and one that only offers a text box makes the reader redo work it
|
|
4000
|
+ |
has already done. The choices are real radio and checkbox inputs styled as chips,
|
|
4001
|
+ |
so selection, keyboard behaviour, and submission are the browser's job.
|
|
4002
|
+ |
</p>
|
|
4003
|
+ |
<Evidence.question
|
|
4004
|
+ |
id="demo-question-single"
|
|
4005
|
+ |
prompt="Which branch should scv-13 work on?"
|
|
4006
|
+ |
description="The repository has three branches with recent commits."
|
|
4007
|
+ |
name="branch"
|
|
4008
|
+ |
selection_mode={:single}
|
|
4009
|
+ |
selected={["main"]}
|
|
4010
|
+ |
text_label="Something else?"
|
|
4011
|
+ |
placeholder="Name a branch"
|
|
4012
|
+ |
submit_label="Use this branch"
|
|
4013
|
+ |
>
|
|
4014
|
+ |
<:option value="main">main</:option>
|
|
4015
|
+ |
<:option value="codex/repository-cli-docs">codex/repository-cli-docs</:option>
|
|
4016
|
+ |
<:option value="feat/ai-elements-catalog">feat/ai-elements-catalog</:option>
|
|
4017
|
+ |
</Evidence.question>
|
|
4018
|
+ |
|
|
4019
|
+ |
<Evidence.question
|
|
4020
|
+ |
id="demo-question-multiple"
|
|
4021
|
+ |
prompt="Which surfaces should the run touch?"
|
|
4022
|
+ |
name="surface"
|
|
4023
|
+ |
selection_mode={:multiple}
|
|
4024
|
+ |
selected={["catalog", "tests"]}
|
|
4025
|
+ |
text="Leave the CSS alone; another run owns it."
|
|
4026
|
+ |
submit_label="Start the run"
|
|
4027
|
+ |
>
|
|
4028
|
+ |
<:option value="catalog">Component catalog</:option>
|
|
4029
|
+ |
<:option value="demos">Demo pages</:option>
|
|
4030
|
+ |
<:option value="tests">Tests</:option>
|
|
4031
|
+ |
<:option value="css">Stylesheet</:option>
|
|
4032
|
+ |
</Evidence.question>
|
|
4033
|
+ |
|
|
4034
|
+ |
<Evidence.question
|
|
4035
|
+ |
id="demo-question-answered"
|
|
4036
|
+ |
prompt="Which branch should scv-13 work on?"
|
|
4037
|
+ |
name="answered_branch"
|
|
4038
|
+ |
selected={["main"]}
|
|
4039
|
+ |
disabled
|
|
4040
|
+ |
submit_label="Answered"
|
|
4041
|
+ |
>
|
|
4042
|
+ |
<:option value="main">main</:option>
|
|
4043
|
+ |
<:option value="codex/repository-cli-docs">codex/repository-cli-docs</:option>
|
|
4044
|
+ |
</Evidence.question>
|
|
4045
|
+ |
<p class="text-sm text-base-content/60">
|
|
4046
|
+ |
The third one is answered and disabled, which is the state the source could not
|
|
4047
|
+ |
reach: its submit control disabled itself from React state, so here the server
|
|
4048
|
+ |
decides, and an answered question stays legible in the transcript.
|
|
4049
|
+ |
</p>
|
|
4050
|
+ |
</div>
|
|
4051
|
+ |
"""
|
|
4052
|
+ |
end
|
|
4053
|
+ |
|
|
4054
|
+ |
defp component_demo(%{item: %{slug: "ai-image"}} = assigns) do
|
|
4055
|
+ |
~H"""
|
|
4056
|
+ |
<div class="space-y-3">
|
|
4057
|
+ |
<p class="text-sm text-base-content/60">
|
|
4058
|
+ |
A generated image cannot travel through Markdown here: the sanitizer's allowlist
|
|
4059
|
+ |
has no <code>img</code>
|
|
4060
|
+ |
in it, so an image written into a model's prose is dropped before it reaches the
|
|
4061
|
+ |
page. It arrives as attributes instead — either a source or the base64 and media
|
|
4062
|
+ |
type a model returns, which become a data URI.
|
|
4063
|
+ |
</p>
|
|
4064
|
+ |
<div class="flex flex-wrap items-start gap-6">
|
|
4065
|
+ |
<Evidence.image
|
|
4066
|
+ |
id="demo-image"
|
|
4067
|
+ |
src={~p"/images/logo.svg"}
|
|
4068
|
+ |
alt="The OpenAgents mark"
|
|
4069
|
+ |
class="max-w-48"
|
|
4070
|
+ |
/>
|
|
4071
|
+ |
<Evidence.image
|
|
4072
|
+ |
id="demo-image-wide"
|
|
4073
|
+ |
src={~p"/images/og-card-default.png"}
|
|
4074
|
+ |
alt="The default social card for openagents.com"
|
|
4075
|
+ |
class="max-w-md"
|
|
4076
|
+ |
/>
|
|
4077
|
+ |
</div>
|
|
4078
|
+ |
<p class="text-sm text-base-content/60">
|
|
4079
|
+ |
<code>alt</code>
|
|
4080
|
+ |
is required rather than defaulted. A generated image is exactly the case where
|
|
4081
|
+ |
nothing nearby says what it shows, so an empty alternative would make the whole
|
|
4082
|
+ |
message disappear for a reader who cannot see it.
|
|
4083
|
+ |
</p>
|
|
4084
|
+ |
</div>
|
|
4085
|
+ |
"""
|
|
4086
|
+ |
end
|
|
4087
|
+ |
|
| 2632 |
4088
|
|
# The breadcrumb names the section a component lives in, so the trail matches
|
| 2633 |
4089
|
|
# the sidebar the reader navigated through.
|
| 2634 |
4090
|
|
defp section_title_for(slug) do
|