Skip to content

Commit 23d1863

Browse files
committed
Minimum viable search results UI
1 parent 0d1a6f5 commit 23d1863

File tree

7 files changed

+224
-56
lines changed

7 files changed

+224
-56
lines changed

typescript/packages/common-os-ui/src/components/os-charm-row.ts

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,15 @@ export class OsCharmRow extends LitElement {
4545
max-width: calc(var(--u) * 90);
4646
}
4747
48+
.charm-row-subtitle {
49+
color: var(--text-secondary);
50+
font-size: 0.9em;
51+
overflow: hidden;
52+
text-overflow: ellipsis;
53+
white-space: nowrap;
54+
max-width: calc(var(--u) * 90);
55+
}
56+
4857
& > .charm-row-extra {
4958
opacity: 0;
5059
transition: opacity var(--dur-md) var(--ease-out-expo);
@@ -63,12 +72,18 @@ export class OsCharmRow extends LitElement {
6372
@property({ type: String })
6473
text = "";
6574

75+
@property({ type: String })
76+
subtitle = "";
77+
6678
override render() {
6779
return html`
6880
<div class="charm-row toolbar">
6981
<div class="hstack gap-sm toolbar-start">
7082
<os-charm-icon class="charm-row-icon" icon="${this.icon}"></os-charm-icon>
71-
<div class="charm-row-text body">${this.text}</div>
83+
<div class="vstack">
84+
<div class="charm-row-text body">${this.text}</div>
85+
${this.subtitle ? html`<div class="charm-row-subtitle">${this.subtitle}</div>` : ""}
86+
</div>
7287
</div>
7388
<div class="hstack gap-sm toolbar-end charm-row-extra">
7489
<os-icon-button-plain icon="more_vert"></os-icon-button-plain>

typescript/packages/lookslike-high-level/src/components/common-spell-editor.ts

Lines changed: 1 addition & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -6,25 +6,7 @@ import { addCharms } from "@commontools/charm";
66
import { tsToExports } from "../localBuild.js";
77
import { iterate, llmTweakSpec, generateSuggestions } from "./spell-ai.js";
88
import { createRef, ref } from "lit/directives/ref.js";
9-
10-
// NOTE(ja): copied from sidebar.ts ... we need a toasty?
11-
const toasty = (message: string) => {
12-
const toastEl = document.createElement("div");
13-
toastEl.textContent = message;
14-
toastEl.style.cssText = `
15-
position: fixed;
16-
top: 20px;
17-
left: 50%;
18-
transform: translateX(-50%);
19-
background: #333;
20-
color: white;
21-
padding: 8px 16px;
22-
border-radius: 4px;
23-
z-index: 1000;
24-
`;
25-
document.body.appendChild(toastEl);
26-
setTimeout(() => toastEl.remove(), 3000);
27-
};
9+
import { toasty } from "./toasty.js";
2810

2911
@customElement("common-spell-editor")
3012
export class CommonSpellEditor extends LitElement {

typescript/packages/lookslike-high-level/src/components/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,3 +14,4 @@ export * as CommonSpellEditor from "./common-spell-editor.js";
1414
export * as CommonMermaid from "./mermaid.js";
1515
export * as CommonPicker from "./picker.js";
1616
export * as CommonCanvasLayout from "./canvas-layout.js";
17+
export * as CommonSearchResults from "./search-results.js";
Lines changed: 140 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,140 @@
1+
import { LitElement, html } from "lit";
2+
import { customElement, property, state } from "lit/decorators.js";
3+
import { repeat } from "lit/directives/repeat.js";
4+
5+
export interface SpellSearchResult {
6+
key: string;
7+
name: string;
8+
description: string;
9+
matchType: string;
10+
compatibleBlobs: {
11+
key: string;
12+
snippet: string;
13+
data: {
14+
count: number;
15+
blobCreatedAt: string;
16+
blobAuthor: string;
17+
};
18+
}[];
19+
}
20+
21+
function formatRelativeTime(dateString: string): string {
22+
const date = new Date(dateString);
23+
const now = new Date();
24+
const diffInMillis = now.getTime() - date.getTime();
25+
const diffInMinutes = Math.floor(diffInMillis / (1000 * 60));
26+
const diffInHours = Math.floor(diffInMillis / (1000 * 60 * 60));
27+
const diffInDays = Math.floor(diffInMillis / (1000 * 60 * 60 * 24));
28+
29+
if (diffInMinutes < 1) {
30+
return "just now";
31+
}
32+
if (diffInMinutes < 60) {
33+
return `${diffInMinutes} minute${diffInMinutes === 1 ? "" : "s"} ago`;
34+
}
35+
if (diffInHours < 24) {
36+
return `${diffInHours} hour${diffInHours === 1 ? "" : "s"} ago`;
37+
}
38+
if (diffInDays < 30) {
39+
return `${diffInDays} day${diffInDays === 1 ? "" : "s"} ago`;
40+
}
41+
42+
return date.toLocaleDateString();
43+
}
44+
45+
@customElement("common-search-results")
46+
export default class SearchResults extends LitElement {
47+
@property({ type: Boolean })
48+
searchOpen = false;
49+
50+
@property({ type: Array })
51+
results: SpellSearchResult[] = [];
52+
53+
@state()
54+
focusedResult: SpellSearchResult | null = null;
55+
56+
private handleClose() {
57+
this.searchOpen = false;
58+
this.dispatchEvent(new CustomEvent("close"));
59+
}
60+
61+
private handleSearch(e: CustomEvent) {
62+
const query = e.detail.query;
63+
this.dispatchEvent(new CustomEvent("search", { detail: { query } }));
64+
}
65+
66+
private handleResultClick(result: SpellSearchResult) {
67+
this.focusedResult = result;
68+
this.dispatchEvent(new CustomEvent("select", { detail: { result } }));
69+
this.searchOpen = false;
70+
}
71+
72+
override render() {
73+
return html`
74+
<style>
75+
.results-grid {
76+
max-height: 50vh;
77+
overflow-y: auto;
78+
}
79+
80+
.result-card {
81+
}
82+
83+
.blob-list {
84+
margin-top: 1rem;
85+
}
86+
87+
.blob-item {
88+
padding: 0.5rem;
89+
border-radius: 4px;
90+
cursor: pointer;
91+
}
92+
93+
.blob-item:hover {
94+
background: #f5f5f5;
95+
}
96+
97+
.blob-meta {
98+
font-size: 0.9em;
99+
color: #666;
100+
margin-top: 0.25rem;
101+
}
102+
</style>
103+
104+
<os-dialog .open=${this.searchOpen} @closedialog=${this.handleClose}>
105+
<div class="results-grid">
106+
${repeat(
107+
this.results,
108+
(result) => result.key,
109+
(result) => html`
110+
<div class="result-card">
111+
<div class="blob-list">
112+
${repeat(
113+
result.compatibleBlobs,
114+
(blob) => blob.key,
115+
(blob) => html`
116+
<os-charm-row
117+
icon="search"
118+
text=${result.description}
119+
subtitle=${`${blob.key}, ${formatRelativeTime(blob.data.blobCreatedAt)}`}
120+
@click=${() =>
121+
this.dispatchEvent(
122+
new CustomEvent("spell-cast", {
123+
detail: {
124+
spell: result,
125+
blob: blob,
126+
},
127+
}),
128+
)}
129+
></os-charm-row>
130+
`,
131+
)}
132+
</div>
133+
</div>
134+
`,
135+
)}
136+
</div>
137+
</os-dialog>
138+
`;
139+
}
140+
}

typescript/packages/lookslike-high-level/src/components/sidebar.ts

Lines changed: 1 addition & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ import { home } from "../recipes/home.jsx";
2222
import { render } from "@commontools/html";
2323
import { saveRecipe } from "../data.js";
2424
import { castNewRecipe } from "./iframe-spell-ai.js";
25+
import { toasty } from "./toasty.js";
2526

2627
const uploadBlob = async (data: any) => {
2728
const id = refer(data).toString();
@@ -35,25 +36,6 @@ const uploadBlob = async (data: any) => {
3536
});
3637
};
3738

38-
// bf: TODO, send a "toast" event on window and an use another element to handle it
39-
const toasty = (message: string) => {
40-
const toastEl = document.createElement("div");
41-
toastEl.textContent = message;
42-
toastEl.style.cssText = `
43-
position: fixed;
44-
top: 20px;
45-
left: 50%;
46-
transform: translateX(-50%);
47-
background: #333;
48-
color: white;
49-
padding: 8px 16px;
50-
border-radius: 4px;
51-
z-index: 1000;
52-
`;
53-
document.body.appendChild(toastEl);
54-
setTimeout(() => toastEl.remove(), 3000);
55-
};
56-
5739
@customElement("common-debug")
5840
export class CommonDebug extends LitElement {
5941
@property({ type: Object })
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
// NOTE(ja): copied from sidebar.ts ... we need a toasty?
2+
export const toasty = (message: string) => {
3+
const toastEl = document.createElement("div");
4+
toastEl.textContent = message;
5+
toastEl.style.cssText = `
6+
position: fixed;
7+
top: 20px;
8+
left: 50%;
9+
transform: translateX(-50%);
10+
background: #333;
11+
color: white;
12+
padding: 8px 16px;
13+
border-radius: 4px;
14+
z-index: 1000;
15+
`;
16+
document.body.appendChild(toastEl);
17+
setTimeout(() => toastEl.remove(), 3000);
18+
};

typescript/packages/lookslike-high-level/src/components/window-manager.ts

Lines changed: 47 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -30,8 +30,10 @@ import { UI, NAME, TYPE } from "@commontools/builder";
3030
import { matchRoute, navigate } from "../router.js";
3131
import { buildRecipe } from "../localBuild.js";
3232
import * as iframeSpellAi from "./iframe-spell-ai.js";
33+
import { SpellSearchResult } from "./search-results.js";
34+
import { toasty } from "./toasty.js";
3335

34-
async function castSpell(value: string, openCharm: (charmId: string) => void) {
36+
async function castSpell(value: string, showResults: (results: SpellSearchResult[]) => void) {
3537
const searchUrl =
3638
typeof window !== "undefined"
3739
? window.location.protocol + "//" + window.location.host + "/api/ai/spell/search"
@@ -48,7 +50,7 @@ async function castSpell(value: string, openCharm: (charmId: string) => void) {
4850
query: value,
4951
tags: [],
5052
options: {
51-
limit: 10,
53+
limit: 32,
5254
includeCompatibility: true,
5355
},
5456
}),
@@ -66,20 +68,7 @@ async function castSpell(value: string, openCharm: (charmId: string) => void) {
6668
} = await response.json();
6769
console.log("Search response:", searchResponse);
6870

69-
let recipeKey = searchResponse.spells?.[0]?.key;
70-
let blob = searchResponse.spells?.[0]?.compatibleBlobs?.[0];
71-
72-
if (recipeKey && blob) {
73-
const recipeId = recipeKey.replace("spell-", "");
74-
await syncRecipe(recipeId);
75-
76-
const recipe = getRecipe(recipeId);
77-
if (!recipe) return;
78-
79-
const charm: DocImpl<Charm> = await runPersistent(recipe, blob.data);
80-
addCharms([charm]);
81-
openCharm(JSON.stringify(charm.entityId));
82-
}
71+
showResults(searchResponse.spells as any);
8372
}
8473
}
8574

@@ -213,6 +202,9 @@ export class CommonWindowManager extends LitElement {
213202
@state()
214203
private focusedProxy: Charm | null = null;
215204

205+
@state()
206+
private spellSearchResults: SpellSearchResult[] = [];
207+
216208
handleUniboxSubmit(event: CustomEvent) {
217209
const value = event.detail.value;
218210
const shiftKey = event.detail.shiftKey;
@@ -221,7 +213,39 @@ export class CommonWindowManager extends LitElement {
221213
if (this.focusedCharm) {
222214
iframeSpellAi.iterate(this.focusedCharm, value, shiftKey);
223215
} else {
224-
castSpell(value, this.openCharm.bind(this));
216+
castSpell(value, this.showResults.bind(this));
217+
}
218+
}
219+
220+
showResults(results: SpellSearchResult[]) {
221+
if (!results || results.length === 0) {
222+
toasty("No results");
223+
return;
224+
}
225+
this.spellSearchResults = results;
226+
}
227+
228+
async onSpellCast(event: CustomEvent) {
229+
const { spell: result, blob } = event.detail;
230+
const recipeKey = result?.key;
231+
232+
if (recipeKey && blob) {
233+
toasty("Syncing...");
234+
const recipeId = recipeKey.replace("spell-", "");
235+
await syncRecipe(recipeId);
236+
237+
const recipe = getRecipe(recipeId);
238+
if (!recipe) return;
239+
240+
toasty("Casting...");
241+
const charm: DocImpl<Charm> = await runPersistent(recipe, blob.data);
242+
addCharms([charm]);
243+
openCharm(JSON.stringify(charm.entityId));
244+
toasty("Ready!");
245+
246+
this.spellSearchResults = [];
247+
} else {
248+
toasty("Failed to cast");
225249
}
226250
}
227251

@@ -351,6 +375,12 @@ export class CommonWindowManager extends LitElement {
351375
</os-charm-chip-group>
352376
</os-dialog>
353377
378+
<common-search-results
379+
.searchOpen=${this.spellSearchResults.length > 0}
380+
.results=${this.spellSearchResults}
381+
@spell-cast=${this.onSpellCast}
382+
></common-search-results>
383+
354384
<os-fabgroup class="pin-br" slot="overlay" @submit=${onAiBoxSubmit}>
355385
${repeat(
356386
Array.isArray(this.suggestions) ? this.suggestions : [],

0 commit comments

Comments
 (0)