Thomas G. Lopes commited on
Commit
a2a70bc
·
1 Parent(s): 59842ba

responsiveness

Browse files
src/lib/components/billing-indicator.svelte CHANGED
@@ -4,16 +4,17 @@
4
  import IconGroup from "~icons/carbon/group";
5
  import IconWarning from "~icons/carbon/warning";
6
  import IconCheckmark from "~icons/carbon/checkmark";
 
7
 
8
  interface Props {
9
  showModal?: () => void;
 
10
  }
11
 
12
- const { showModal }: Props = $props();
13
  </script>
14
 
15
- <button onclick={showModal} class="btn-sm">
16
- <!-- Avatar or Icon -->
17
  {#if billing.organization && billing.organizationInfo?.avatar && billing.isValid}
18
  <img src={billing.organizationInfo.avatar} alt={billing.displayName} class="size-4 rounded-full" />
19
  {:else if billing.organization}
@@ -21,10 +22,9 @@
21
  {:else}
22
  <IconUser class="size-4" />
23
  {/if}
 
24
 
25
- <span class="max-w-32 truncate">{billing.displayName}</span>
26
-
27
- <!-- Status indicator for organization -->
28
  {#if billing.organization}
29
  {#if billing.validating}
30
  <div class="size-3 animate-spin rounded-full border border-yellow-600 border-t-transparent"></div>
@@ -34,4 +34,22 @@
34
  <IconWarning class="size-3 text-red-600 dark:text-red-400" />
35
  {/if}
36
  {/if}
37
- </button>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
4
  import IconGroup from "~icons/carbon/group";
5
  import IconWarning from "~icons/carbon/warning";
6
  import IconCheckmark from "~icons/carbon/checkmark";
7
+ import Tooltip from "./tooltip.svelte";
8
 
9
  interface Props {
10
  showModal?: () => void;
11
+ iconOnly?: boolean;
12
  }
13
 
14
+ const { showModal, iconOnly = false }: Props = $props();
15
  </script>
16
 
17
+ {#snippet icon()}
 
18
  {#if billing.organization && billing.organizationInfo?.avatar && billing.isValid}
19
  <img src={billing.organizationInfo.avatar} alt={billing.displayName} class="size-4 rounded-full" />
20
  {:else if billing.organization}
 
22
  {:else}
23
  <IconUser class="size-4" />
24
  {/if}
25
+ {/snippet}
26
 
27
+ {#snippet statusIndicator()}
 
 
28
  {#if billing.organization}
29
  {#if billing.validating}
30
  <div class="size-3 animate-spin rounded-full border border-yellow-600 border-t-transparent"></div>
 
34
  <IconWarning class="size-3 text-red-600 dark:text-red-400" />
35
  {/if}
36
  {/if}
37
+ {/snippet}
38
+
39
+ {#if iconOnly}
40
+ <Tooltip>
41
+ {#snippet trigger(tooltip)}
42
+ <button onclick={showModal} class="btn-sm size-8 shrink-0 p-0!" {...tooltip.trigger}>
43
+ {@render icon()}
44
+ {@render statusIndicator()}
45
+ </button>
46
+ {/snippet}
47
+ {billing.displayName}
48
+ </Tooltip>
49
+ {:else}
50
+ <button onclick={showModal} class="btn-sm">
51
+ {@render icon()}
52
+ <span class="max-w-32 truncate">{billing.displayName}</span>
53
+ {@render statusIndicator()}
54
+ </button>
55
+ {/if}
src/lib/components/inference-playground/model-selector.svelte CHANGED
@@ -50,7 +50,8 @@
50
  <div class="flex items-center gap-2 overflow-hidden">
51
  <Avatar model={conversation.model} orgName={nameSpace} size="sm" />
52
  <div class="overflow-hidden">
53
- <span class="text-xs text-gray-500 dark:text-gray-400">{nameSpace}/</span>
 
54
  <span class="truncate font-medium">{modelName}</span>
55
  </div>
56
  </div>
 
50
  <div class="flex items-center gap-2 overflow-hidden">
51
  <Avatar model={conversation.model} orgName={nameSpace} size="sm" />
52
  <div class="overflow-hidden">
53
+ <!-- Hide org name on mobile -->
54
+ <span class="hidden text-xs text-gray-500 md:inline dark:text-gray-400">{nameSpace}/</span>
55
  <span class="truncate font-medium">{modelName}</span>
56
  </div>
57
  </div>
src/lib/components/inference-playground/playground.svelte CHANGED
@@ -30,6 +30,8 @@
30
  import ModelSelector from "./model-selector.svelte";
31
  import ProjectTreeSidebar from "./project-tree-sidebar.svelte";
32
  import ProviderSelect from "./provider-select.svelte";
 
 
33
 
34
  // LocalStorage keys
35
  const SIDEBAR_COLLAPSED_KEY = "playground:sidebar:collapsed";
@@ -53,9 +55,11 @@
53
  let viewCode = $state(false);
54
  let sidebarCollapsed = $state(get_initial_collapsed());
55
  let sidebarWidth = $state(get_initial_width());
 
56
  let billingModalOpen = $state(false);
57
  let selectCompareModelOpen = $state(false);
58
  let settingsPopoverOpen = $state(false);
 
59
 
60
  const compareActive = $derived(conversations.active.length === 2);
61
 
@@ -91,6 +95,14 @@
91
  settingsPopoverOpen = value;
92
  },
93
  });
 
 
 
 
 
 
 
 
94
  </script>
95
 
96
  <div
@@ -99,44 +111,78 @@
99
  "dark:bg-gray-900 dark:text-gray-300 dark:[color-scheme:dark]",
100
  ]}
101
  >
102
- <!-- Project tree sidebar -->
103
- <ProjectTreeSidebar
104
- collapsed={sidebarCollapsed}
105
- width={sidebarWidth}
106
- onToggleCollapse={toggle_sidebar_collapsed}
107
- onWidthChange={handle_sidebar_width_change}
108
- />
 
 
 
 
 
 
 
109
 
110
  <!-- Main content area -->
111
  <div class="relative flex flex-1 flex-col overflow-hidden">
112
  <!-- Top bar -->
113
  <header
114
- class="flex justify-between border-b border-gray-200 bg-white px-4 py-2 dark:border-gray-800 dark:bg-gray-900"
115
  >
116
- <!-- Left side: Model selector, provider selector, and compare -->
117
- <div class="flex items-start gap-3">
118
- {#if !compareActive && conversations.active[0]}
119
- <!-- Model and provider stacked vertically with fixed width -->
120
- <div class="flex w-72 flex-col gap-1">
121
- <ModelSelector conversation={conversations.active[0]} compact />
122
- {#if isHFModel(conversations.active[0].model)}
 
 
 
 
 
 
 
 
 
123
  <!-- eslint-disable-next-line @typescript-eslint/no-explicit-any -->
124
  <ProviderSelect conversation={conversations.active[0] as any} compact />
125
- {/if}
126
- </div>
127
- <button class="btn-xs" onclick={() => (selectCompareModelOpen = true)}>
128
- <IconCompare class="size-4" />
129
- Compare
130
- </button>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
131
  {/if}
132
- </div>
133
 
134
- <!-- Right side: Actions -->
135
- <div class="flex items-center gap-2">
136
- <!-- Checkpoints menu -->
137
  <CheckpointsMenu />
138
 
139
- <!-- Settings button with popover -->
140
  <Tooltip>
141
  {#snippet trigger(tooltip)}
142
  <button
@@ -151,14 +197,19 @@
151
  Settings
152
  </Tooltip>
153
 
154
- <!-- Share button -->
155
- <button onclick={() => projects.current && showShareModal(projects.current)} class="btn-sm">
156
  <IconShare class="size-4" />
157
- Share
158
  </button>
159
 
160
- <!-- Billing indicator -->
161
- <BillingIndicator showModal={() => (billingModalOpen = true)} />
 
 
 
 
 
162
  </div>
163
  </header>
164
 
@@ -191,9 +242,9 @@
191
 
192
  <!-- Bottom bar -->
193
  <div
194
- class="relative mt-2 flex h-12 shrink-0 items-center justify-between border-t border-gray-200 bg-white px-4 dark:border-gray-800 dark:bg-gray-900"
195
  >
196
- <div class="flex items-center gap-4">
197
  <Tooltip>
198
  {#snippet trigger(tooltip)}
199
  <button
@@ -209,8 +260,8 @@
209
  Clear conversation
210
  </Tooltip>
211
 
212
- <!-- Footer links - moved here to avoid overlap -->
213
- <div class="flex items-center gap-2 text-xs max-md:hidden">
214
  <a
215
  target="_blank"
216
  href="https://huggingface.co/docs/inference-providers/tasks/chat-completion"
@@ -239,7 +290,7 @@
239
  </div>
240
  </div>
241
 
242
- <!-- Stats in center -->
243
  <div
244
  class="pointer-events-none absolute inset-x-0 flex items-center justify-center gap-x-8 text-center text-sm text-gray-500 max-xl:hidden"
245
  >
@@ -253,7 +304,7 @@
253
  <div class="flex items-center gap-2">
254
  <button type="button" onclick={() => (viewCode = !viewCode)} class="btn h-[28px]! px-2!">
255
  <IconCode />
256
- {!viewCode ? "View Code" : "Hide Code"}
257
  </button>
258
  </div>
259
  </div>
@@ -261,6 +312,18 @@
261
  </div>
262
  </div>
263
 
 
 
 
 
 
 
 
 
 
 
 
 
264
  <!-- Settings popover content -->
265
  <div
266
  {...settingsPopover.content}
 
30
  import ModelSelector from "./model-selector.svelte";
31
  import ProjectTreeSidebar from "./project-tree-sidebar.svelte";
32
  import ProviderSelect from "./provider-select.svelte";
33
+ import IconMenu from "~icons/carbon/menu";
34
+ import IconProvider from "../icon-provider.svelte";
35
 
36
  // LocalStorage keys
37
  const SIDEBAR_COLLAPSED_KEY = "playground:sidebar:collapsed";
 
55
  let viewCode = $state(false);
56
  let sidebarCollapsed = $state(get_initial_collapsed());
57
  let sidebarWidth = $state(get_initial_width());
58
+ let mobileSidebarOpen = $state(false);
59
  let billingModalOpen = $state(false);
60
  let selectCompareModelOpen = $state(false);
61
  let settingsPopoverOpen = $state(false);
62
+ let providerPopoverOpen = $state(false);
63
 
64
  const compareActive = $derived(conversations.active.length === 2);
65
 
 
95
  settingsPopoverOpen = value;
96
  },
97
  });
98
+
99
+ // Provider popover (mobile)
100
+ const providerPopover = new Popover({
101
+ open: () => providerPopoverOpen,
102
+ onOpenChange: value => {
103
+ providerPopoverOpen = value;
104
+ },
105
+ });
106
  </script>
107
 
108
  <div
 
111
  "dark:bg-gray-900 dark:text-gray-300 dark:[color-scheme:dark]",
112
  ]}
113
  >
114
+ <!-- Project tree sidebar (hidden on mobile unless open) -->
115
+ <div class="max-md:hidden">
116
+ <ProjectTreeSidebar
117
+ collapsed={sidebarCollapsed}
118
+ width={sidebarWidth}
119
+ onToggleCollapse={toggle_sidebar_collapsed}
120
+ onWidthChange={handle_sidebar_width_change}
121
+ />
122
+ </div>
123
+
124
+ <!-- Mobile sidebar (overlay) -->
125
+ <div class="md:hidden">
126
+ <ProjectTreeSidebar mobileOpen={mobileSidebarOpen} onMobileClose={() => (mobileSidebarOpen = false)} />
127
+ </div>
128
 
129
  <!-- Main content area -->
130
  <div class="relative flex flex-1 flex-col overflow-hidden">
131
  <!-- Top bar -->
132
  <header
133
+ class="flex items-center gap-2 border-b border-gray-200 bg-white px-2 py-2 md:px-4 dark:border-gray-800 dark:bg-gray-900"
134
  >
135
+ <!-- Mobile menu button -->
136
+ <button
137
+ class="btn-sm size-8 shrink-0 p-0! md:hidden"
138
+ onclick={() => (mobileSidebarOpen = true)}
139
+ aria-label="Open menu"
140
+ >
141
+ <IconMenu class="size-4" />
142
+ </button>
143
+
144
+ {#if !compareActive && conversations.active[0]}
145
+ <!-- Model and provider stacked vertically on desktop -->
146
+ <div class="flex min-w-0 flex-1 flex-col gap-1 md:w-72 md:flex-none">
147
+ <ModelSelector conversation={conversations.active[0]} compact />
148
+ <!-- Desktop: Provider selector below model -->
149
+ {#if isHFModel(conversations.active[0].model)}
150
+ <div class="hidden md:block">
151
  <!-- eslint-disable-next-line @typescript-eslint/no-explicit-any -->
152
  <ProviderSelect conversation={conversations.active[0] as any} compact />
153
+ </div>
154
+ {/if}
155
+ </div>
156
+
157
+ <!-- Desktop: Compare button -->
158
+ <button class="btn-xs self-start max-md:hidden" onclick={() => (selectCompareModelOpen = true)}>
159
+ <IconCompare class="size-4" />
160
+ Compare
161
+ </button>
162
+ {/if}
163
+
164
+ <!-- Right side actions -->
165
+ <div class="ml-auto flex shrink-0 items-center gap-1 md:gap-2">
166
+ <!-- Mobile: Provider popover button -->
167
+ {#if !compareActive && conversations.active[0] && isHFModel(conversations.active[0].model)}
168
+ {@const activeConv = conversations.active[0]}
169
+ <Tooltip>
170
+ {#snippet trigger(tooltip)}
171
+ <button
172
+ {...providerPopover.trigger}
173
+ class="btn-sm size-8 shrink-0 p-0! md:hidden"
174
+ aria-label="Provider settings"
175
+ {...tooltip.trigger}
176
+ >
177
+ <IconProvider provider={activeConv.data.provider ?? "auto"} />
178
+ </button>
179
+ {/snippet}
180
+ Provider
181
+ </Tooltip>
182
  {/if}
 
183
 
 
 
 
184
  <CheckpointsMenu />
185
 
 
186
  <Tooltip>
187
  {#snippet trigger(tooltip)}
188
  <button
 
197
  Settings
198
  </Tooltip>
199
 
200
+ <!-- Desktop: Share button -->
201
+ <button onclick={() => projects.current && showShareModal(projects.current)} class="btn-sm max-md:hidden">
202
  <IconShare class="size-4" />
203
+ <span class="max-lg:hidden">Share</span>
204
  </button>
205
 
206
+ <!-- Billing: icon only on mobile -->
207
+ <div class="md:hidden">
208
+ <BillingIndicator showModal={() => (billingModalOpen = true)} iconOnly />
209
+ </div>
210
+ <div class="hidden md:block">
211
+ <BillingIndicator showModal={() => (billingModalOpen = true)} />
212
+ </div>
213
  </div>
214
  </header>
215
 
 
242
 
243
  <!-- Bottom bar -->
244
  <div
245
+ class="relative mt-2 flex h-12 shrink-0 items-center justify-between gap-2 border-t border-gray-200 bg-white px-2 md:px-4 dark:border-gray-800 dark:bg-gray-900"
246
  >
247
+ <div class="flex items-center gap-2 md:gap-4">
248
  <Tooltip>
249
  {#snippet trigger(tooltip)}
250
  <button
 
260
  Clear conversation
261
  </Tooltip>
262
 
263
+ <!-- Footer links (hidden on mobile) -->
264
+ <div class="flex items-center gap-2 text-xs max-lg:hidden">
265
  <a
266
  target="_blank"
267
  href="https://huggingface.co/docs/inference-providers/tasks/chat-completion"
 
290
  </div>
291
  </div>
292
 
293
+ <!-- Stats in center (hidden on smaller screens) -->
294
  <div
295
  class="pointer-events-none absolute inset-x-0 flex items-center justify-center gap-x-8 text-center text-sm text-gray-500 max-xl:hidden"
296
  >
 
304
  <div class="flex items-center gap-2">
305
  <button type="button" onclick={() => (viewCode = !viewCode)} class="btn h-[28px]! px-2!">
306
  <IconCode />
307
+ <span class="max-sm:hidden">{!viewCode ? "View Code" : "Hide Code"}</span>
308
  </button>
309
  </div>
310
  </div>
 
312
  </div>
313
  </div>
314
 
315
+ <!-- Provider popover content (mobile) -->
316
+ <div
317
+ {...providerPopover.content}
318
+ class="z-50 w-64 rounded-xl border border-gray-200 bg-white p-3 shadow-lg dark:border-gray-700 dark:bg-gray-800"
319
+ >
320
+ <h3 class="mb-3 text-sm font-semibold text-gray-700 uppercase dark:text-gray-300">Provider</h3>
321
+ {#if conversations.active[0] && isHFModel(conversations.active[0].model)}
322
+ <!-- eslint-disable-next-line @typescript-eslint/no-explicit-any -->
323
+ <ProviderSelect conversation={conversations.active[0] as any} />
324
+ {/if}
325
+ </div>
326
+
327
  <!-- Settings popover content -->
328
  <div
329
  {...settingsPopover.content}
src/lib/components/inference-playground/project-tree-sidebar.svelte CHANGED
@@ -15,7 +15,7 @@
15
  import IconEdit from "~icons/carbon/edit";
16
  import IconDelete from "~icons/carbon/trash-can";
17
  import IconHistory from "~icons/carbon/recently-viewed";
18
- import IconSidebarCollapse from "~icons/carbon/side-panel-close";
19
  import IconSidebarExpand from "~icons/carbon/side-panel-open";
20
  import { prompt } from "../prompts.svelte";
21
  import Tooltip from "../tooltip.svelte";
@@ -34,12 +34,21 @@
34
 
35
  interface Props {
36
  collapsed?: boolean;
 
37
  width?: number;
38
  onToggleCollapse?: () => void;
 
39
  onWidthChange?: (width: number) => void;
40
  }
41
 
42
- let { collapsed = false, width = DEFAULT_WIDTH, onToggleCollapse, onWidthChange }: Props = $props();
 
 
 
 
 
 
 
43
 
44
  // Resize state
45
  let is_resizing = $state(false);
@@ -182,10 +191,24 @@
182
  }
183
  </script>
184
 
 
 
 
 
 
 
 
 
 
 
185
  <aside
186
  class={cn(
187
- "relative flex h-full flex-col overflow-hidden border-r border-gray-200 bg-gray-50/50 dark:border-gray-800 dark:bg-gray-900/50",
188
- !is_resizing && "transition-[width] duration-200 ease-out",
 
 
 
 
189
  )}
190
  style="width: {collapsed ? COLLAPSED_WIDTH : width}px"
191
  >
@@ -206,19 +229,28 @@
206
  {/snippet}
207
  New project
208
  </Tooltip>
 
209
  <Tooltip>
210
  {#snippet trigger(tooltip)}
211
  <button
212
  onclick={onToggleCollapse}
213
- class="rounded p-1 text-gray-500 hover:bg-gray-200 hover:text-gray-700 dark:hover:bg-gray-700 dark:hover:text-gray-200"
214
  aria-label="Collapse sidebar"
215
  {...tooltip.trigger}
216
  >
217
- <IconSidebarCollapse class="size-4" />
218
  </button>
219
  {/snippet}
220
  Collapse sidebar
221
  </Tooltip>
 
 
 
 
 
 
 
 
222
  </div>
223
  </div>
224
 
@@ -285,12 +317,12 @@
285
  </div>
286
  {/if}
287
 
288
- <!-- Resize handle -->
289
  {#if !collapsed}
290
  <!-- svelte-ignore a11y_no_static_element_interactions -->
291
  <div
292
  class={cn(
293
- "absolute top-0 right-0 h-full w-1 cursor-ew-resize transition-colors",
294
  "hover:bg-blue-400 dark:hover:bg-blue-500",
295
  is_resizing && "bg-blue-500 dark:bg-blue-400",
296
  )}
 
15
  import IconEdit from "~icons/carbon/edit";
16
  import IconDelete from "~icons/carbon/trash-can";
17
  import IconHistory from "~icons/carbon/recently-viewed";
18
+ import IconClose from "~icons/carbon/close";
19
  import IconSidebarExpand from "~icons/carbon/side-panel-open";
20
  import { prompt } from "../prompts.svelte";
21
  import Tooltip from "../tooltip.svelte";
 
34
 
35
  interface Props {
36
  collapsed?: boolean;
37
+ mobileOpen?: boolean;
38
  width?: number;
39
  onToggleCollapse?: () => void;
40
+ onMobileClose?: () => void;
41
  onWidthChange?: (width: number) => void;
42
  }
43
 
44
+ let {
45
+ collapsed = false,
46
+ mobileOpen = false,
47
+ width = DEFAULT_WIDTH,
48
+ onToggleCollapse,
49
+ onMobileClose,
50
+ onWidthChange,
51
+ }: Props = $props();
52
 
53
  // Resize state
54
  let is_resizing = $state(false);
 
191
  }
192
  </script>
193
 
194
+ <!-- Mobile overlay backdrop -->
195
+ {#if mobileOpen}
196
+ <!-- svelte-ignore a11y_no_static_element_interactions -->
197
+ <div
198
+ class="fixed inset-0 z-40 bg-black/50 md:hidden"
199
+ onclick={onMobileClose}
200
+ onkeydown={e => e.key === "Escape" && onMobileClose?.()}
201
+ ></div>
202
+ {/if}
203
+
204
  <aside
205
  class={cn(
206
+ "relative flex h-full flex-col overflow-hidden border-r border-gray-200 bg-gray-50 dark:border-gray-800 dark:bg-gray-900",
207
+ !is_resizing && "transition-[width,transform] duration-200 ease-out",
208
+ // Desktop: normal sidebar behavior
209
+ "max-md:fixed max-md:top-0 max-md:left-0 max-md:z-50 max-md:w-72!",
210
+ // Mobile: slide in/out
211
+ mobileOpen ? "max-md:translate-x-0" : "max-md:-translate-x-full",
212
  )}
213
  style="width: {collapsed ? COLLAPSED_WIDTH : width}px"
214
  >
 
229
  {/snippet}
230
  New project
231
  </Tooltip>
232
+ <!-- Desktop: collapse button -->
233
  <Tooltip>
234
  {#snippet trigger(tooltip)}
235
  <button
236
  onclick={onToggleCollapse}
237
+ class="hidden rounded p-1 text-gray-500 hover:bg-gray-200 hover:text-gray-700 md:block dark:hover:bg-gray-700 dark:hover:text-gray-200"
238
  aria-label="Collapse sidebar"
239
  {...tooltip.trigger}
240
  >
241
+ <IconClose class="size-4" />
242
  </button>
243
  {/snippet}
244
  Collapse sidebar
245
  </Tooltip>
246
+ <!-- Mobile: close button -->
247
+ <button
248
+ onclick={onMobileClose}
249
+ class="rounded p-1 text-gray-500 hover:bg-gray-200 hover:text-gray-700 md:hidden dark:hover:bg-gray-700 dark:hover:text-gray-200"
250
+ aria-label="Close sidebar"
251
+ >
252
+ <IconClose class="size-4" />
253
+ </button>
254
  </div>
255
  </div>
256
 
 
317
  </div>
318
  {/if}
319
 
320
+ <!-- Resize handle (desktop only) -->
321
  {#if !collapsed}
322
  <!-- svelte-ignore a11y_no_static_element_interactions -->
323
  <div
324
  class={cn(
325
+ "absolute top-0 right-0 h-full w-1 cursor-ew-resize transition-colors max-md:hidden",
326
  "hover:bg-blue-400 dark:hover:bg-blue-500",
327
  is_resizing && "bg-blue-500 dark:bg-blue-400",
328
  )}