@olares/lares-mobile 0.15.4 → 0.15.6

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (44) hide show
  1. package/README.md +6 -2
  2. package/package.json +2 -2
  3. package/src/App.vue +924 -184
  4. package/src/chat/ChatBar.vue +2 -2
  5. package/src/chat/Composer.vue +467 -16
  6. package/src/chat/Deliverables.vue +22 -10
  7. package/src/chat/HistoryPanel.vue +747 -55
  8. package/src/chat/MessageActions.vue +88 -34
  9. package/src/chat/QuestionCard.vue +5 -5
  10. package/src/chat/StageRow.vue +8 -5
  11. package/src/chat/Transcript.vue +20 -6
  12. package/src/chat/composer-tokens.js +32 -0
  13. package/src/chat/format.js +37 -0
  14. package/src/desktop/DesktopComposer.vue +871 -0
  15. package/src/desktop/DesktopContextMeter.vue +81 -0
  16. package/src/desktop/DesktopGoal.vue +88 -0
  17. package/src/desktop/DesktopPreview.vue +58 -0
  18. package/src/desktop/DesktopPreviewTabs.vue +82 -0
  19. package/src/desktop/DesktopQuestions.vue +118 -0
  20. package/src/desktop/DesktopQueue.vue +203 -0
  21. package/src/desktop/DesktopShell.vue +203 -0
  22. package/src/desktop/DesktopSidebar.vue +645 -0
  23. package/src/desktop/DesktopTodos.vue +52 -0
  24. package/src/desktop/DirectoryBrowser.vue +170 -0
  25. package/src/desktop/DirectoryNewFolder.vue +124 -0
  26. package/src/desktop/ui/PcDialog.vue +85 -0
  27. package/src/desktop/ui/PcPopover.vue +110 -0
  28. package/src/desktop/ui/PcTooltip.vue +95 -0
  29. package/src/desktop/ui/place.js +24 -0
  30. package/src/desktop/workspace-view.js +121 -0
  31. package/src/host.js +20 -2
  32. package/src/index.d.ts +2 -0
  33. package/src/mobile/MobileDirectoryNewFolder.vue +121 -0
  34. package/src/mobile/MobileDirectorySheet.vue +162 -0
  35. package/src/mobile/MobileNewSession.vue +217 -0
  36. package/src/mobile/MobileShell.vue +367 -0
  37. package/src/motion.css +166 -0
  38. package/src/preview/Preview.vue +91 -150
  39. package/src/preview/PreviewBody.vue +185 -0
  40. package/src/settings/AgentSettings.vue +126 -36
  41. package/src/settings/Select.vue +183 -0
  42. package/src/settings/SettingRow.vue +5 -4
  43. package/src/settings/Sheet.vue +4 -4
  44. package/src/styles.css +9 -0
@@ -0,0 +1,121 @@
1
+ import {
2
+ FLAT_SESSION_ORDER_KEY,
3
+ nextSessionOrderAccount,
4
+ } from "@olares/lares-core/larepass/workspace";
5
+
6
+ const STORAGE_KEY = "lares.larepass.workspace.view.v1";
7
+
8
+ const initialView = () => ({
9
+ groupBy: "workspace",
10
+ orderBy: "updated",
11
+ groupExpansion: {},
12
+ sessionOrderByAccount: {},
13
+ sessionUpdatedAtByAccount: {},
14
+ });
15
+
16
+ export function loadWorkspaceView(storage = globalThis.localStorage) {
17
+ if (!storage) return initialView();
18
+ try {
19
+ const saved = JSON.parse(storage.getItem(STORAGE_KEY) || "{}");
20
+ return {
21
+ groupBy: saved.groupBy === "flat" ? "flat" : "workspace",
22
+ orderBy: saved.orderBy === "manual" ? "manual" : "updated",
23
+ groupExpansion: saved.groupExpansion && typeof saved.groupExpansion === "object"
24
+ ? saved.groupExpansion
25
+ : {},
26
+ sessionOrderByAccount: saved.sessionOrderByAccount && typeof saved.sessionOrderByAccount === "object"
27
+ ? saved.sessionOrderByAccount
28
+ : {},
29
+ sessionUpdatedAtByAccount: saved.sessionUpdatedAtByAccount && typeof saved.sessionUpdatedAtByAccount === "object"
30
+ ? saved.sessionUpdatedAtByAccount
31
+ : {},
32
+ };
33
+ } catch {
34
+ return initialView();
35
+ }
36
+ }
37
+
38
+ export function saveWorkspaceView(view, storage = globalThis.localStorage) {
39
+ if (!storage) return;
40
+ try {
41
+ storage.setItem(STORAGE_KEY, JSON.stringify(view));
42
+ } catch {
43
+ // Browser storage can be unavailable in private or embedded contexts.
44
+ }
45
+ }
46
+
47
+ function accountSessionIds(sessions, workspaces) {
48
+ const known = new Set(sessions.map((row) => row.sessionId));
49
+ const accounted = new Set();
50
+ const accounts = workspaces.map((workspace) => {
51
+ const ids = (workspace.sessionIds ?? []).filter((id) => known.has(id));
52
+ ids.forEach((id) => accounted.add(id));
53
+ return [workspace.workspaceId, ids];
54
+ });
55
+ accounts.push(["", sessions.map((row) => row.sessionId).filter((id) => !accounted.has(id))]);
56
+ accounts.push([FLAT_SESSION_ORDER_KEY, sessions.map((row) => row.sessionId)]);
57
+ return accounts;
58
+ }
59
+
60
+ function promote(order, sessionId) {
61
+ return [sessionId, ...order.filter((id) => id !== sessionId)];
62
+ }
63
+
64
+ export function syncWorkspaceViewOrders(view, {
65
+ sessions = [],
66
+ workspaces = [],
67
+ currentSessionId = "",
68
+ sortByRecency = false,
69
+ } = {}) {
70
+ const nextOrder = {};
71
+ const nextUpdatedAt = {};
72
+ for (const [key, sessionIds] of accountSessionIds(sessions, workspaces)) {
73
+ const next = nextSessionOrderAccount({
74
+ sessionIds,
75
+ previousOrder: view.sessionOrderByAccount[key],
76
+ previousUpdatedAt: view.sessionUpdatedAtByAccount[key],
77
+ sessions,
78
+ orderBy: view.orderBy,
79
+ sortByRecency: view.orderBy === "updated"
80
+ && (sortByRecency || view.sessionOrderByAccount[key] === undefined),
81
+ });
82
+ nextOrder[key] = next.order;
83
+ nextUpdatedAt[key] = next.updatedAt;
84
+ }
85
+
86
+ const current = sessions.find((row) => row.sessionId === currentSessionId);
87
+ if (current?.blank) {
88
+ const account = workspaces.find((workspace) => (
89
+ workspace.sessionIds?.includes(currentSessionId)
90
+ ))?.workspaceId ?? "";
91
+ nextOrder[account] = promote(nextOrder[account] ?? [], currentSessionId);
92
+ nextOrder[FLAT_SESSION_ORDER_KEY] = promote(
93
+ nextOrder[FLAT_SESSION_ORDER_KEY] ?? [],
94
+ currentSessionId,
95
+ );
96
+ }
97
+
98
+ return {
99
+ ...view,
100
+ sessionOrderByAccount: nextOrder,
101
+ sessionUpdatedAtByAccount: nextUpdatedAt,
102
+ };
103
+ }
104
+
105
+ /**
106
+ * Reordering is expressed as "insert before X", while a pointer lands on an edge
107
+ * of a row. Translate the hovered row plus edge into that anchor; "" is the end
108
+ * of the list, which every insert-before call reads as an append.
109
+ */
110
+ export function dropAnchor(ids, hint) {
111
+ const at = ids.indexOf(hint?.key);
112
+ if (at === -1) return "";
113
+ return hint.edge === "after" ? ids[at + 1] ?? "" : hint.key;
114
+ }
115
+
116
+ export function insertSessionBefore(order, sessionId, beforeSessionId) {
117
+ const next = order.filter((id) => id !== sessionId);
118
+ const index = beforeSessionId ? next.indexOf(beforeSessionId) : -1;
119
+ next.splice(index === -1 ? next.length : index, 0, sessionId);
120
+ return next;
121
+ }
package/src/host.js CHANGED
@@ -1,5 +1,7 @@
1
1
  import { hostTarget, hostUrl, MODELS_PATH, probeHost } from "@olares/lares-core/larepass/host";
2
2
  import { callRpc, consumeMux, ensureSession, loadTranscript, MUX_PATH, sendPrompt } from "@olares/lares-core/larepass/chat";
3
+ import { commandsUrl, normalizeCommands } from "@olares/lares-core/larepass/commands";
4
+ import { normalizeReferenceCandidates, referencesUrl } from "@olares/lares-core/larepass/references";
3
5
  import { muxWsUrl } from "@olares/lares-core/larepass/mux";
4
6
  import { RESPOND_PATH } from "@olares/lares-core/larepass/rpc";
5
7
  import { createHostSettings } from "@olares/lares-core/larepass/settings";
@@ -133,8 +135,8 @@ export function createHostClient(ports = {}) {
133
135
  history(sessionId) {
134
136
  return loadTranscript(rpc, sessionId);
135
137
  },
136
- prompt(sessionId, text) {
137
- return sendPrompt(rpc, sessionId, text);
138
+ prompt(sessionId, content, mode) {
139
+ return sendPrompt(rpc, sessionId, content, mode);
138
140
  },
139
141
  async respond(message) {
140
142
  const res = await request(RESPOND_PATH, { method: "POST", body: message });
@@ -142,6 +144,22 @@ export function createHostClient(ports = {}) {
142
144
  if (body && typeof body === "object" && typeof body.accepted === "boolean") return body;
143
145
  return { accepted: Boolean(res?.ok), reason: res?.ok ? undefined : "bad-response" };
144
146
  },
147
+ async commands(sessionId) {
148
+ const res = await request(commandsUrl(sessionId));
149
+ const http = Number(res?.status) || 0;
150
+ if (http < 200 || http >= 300) {
151
+ throw new Error(res?.body?.error?.code || `commands ${http || "failed"}`);
152
+ }
153
+ return normalizeCommands(res?.body);
154
+ },
155
+ async references(sessionId, query, quoted = false) {
156
+ const res = await request(referencesUrl(sessionId, query));
157
+ const http = Number(res?.status) || 0;
158
+ if (http < 200 || http >= 300) {
159
+ throw new Error(res?.body?.error?.code || `references ${http || "failed"}`);
160
+ }
161
+ return normalizeReferenceCandidates(res?.body, quoted);
162
+ },
145
163
  async preview(sessionId, path) {
146
164
  const res = await request(previewMetaUrl(sessionId, path));
147
165
  const http = Number(res?.status) || 0;
package/src/index.d.ts CHANGED
@@ -71,10 +71,12 @@ export const LaresApp: DefineComponent<{
71
71
  proxyPrefix?: string;
72
72
  env?: HostEnv;
73
73
  request?: (url: string, init?: HostRequestInit) => Promise<HostResponse>;
74
+ socketProtocol?: HostPorts["socketProtocol"];
74
75
  }>;
75
76
 
76
77
  export const LaresAgentSettings: DefineComponent<{
77
78
  locale?: string;
79
+ device?: ChatDevice;
78
80
  baseUrl?: string;
79
81
  proxyPrefix?: string;
80
82
  env?: HostEnv;
@@ -0,0 +1,121 @@
1
+ <template>
2
+ <LaresSheet :open="open" :title="t('desktop.newFolder')" @close="$emit('close')">
3
+ <div class="mobile-directory-new">
4
+ <input
5
+ ref="input"
6
+ v-model="value"
7
+ class="mobile-directory-new__input"
8
+ :placeholder="t('desktop.untitledFolder')"
9
+ :disabled="busy"
10
+ @keydown.enter.prevent="submit"
11
+ />
12
+ <div class="mobile-directory-new__actions">
13
+ <button type="button" class="mobile-directory-new__cancel" :disabled="busy" @click="$emit('close')">
14
+ {{ t("desktop.cancel") }}
15
+ </button>
16
+ <button
17
+ type="button"
18
+ class="mobile-directory-new__create"
19
+ :disabled="busy || !value.trim()"
20
+ @click="submit"
21
+ >
22
+ {{ t("desktop.createFolder") }}
23
+ </button>
24
+ </div>
25
+ </div>
26
+ </LaresSheet>
27
+ </template>
28
+
29
+ <script>
30
+ import LaresSheet from "../settings/Sheet.vue";
31
+
32
+ export default {
33
+ name: "LaresMobileDirectoryNewFolder",
34
+ components: { LaresSheet },
35
+ props: {
36
+ open: { type: Boolean, default: false },
37
+ busy: { type: Boolean, default: false },
38
+ t: { type: Function, required: true },
39
+ },
40
+ emits: ["close", "create"],
41
+ data() {
42
+ return { value: "" };
43
+ },
44
+ watch: {
45
+ open(value) {
46
+ if (!value) {
47
+ this.value = "";
48
+ return;
49
+ }
50
+ this.value = this.t("desktop.untitledFolder");
51
+ this.$nextTick(() => {
52
+ this.$refs.input?.focus();
53
+ this.$refs.input?.select();
54
+ });
55
+ },
56
+ },
57
+ methods: {
58
+ submit() {
59
+ const name = this.value.trim();
60
+ if (!name || this.busy) return;
61
+ this.$emit("create", name);
62
+ },
63
+ },
64
+ };
65
+ </script>
66
+
67
+ <style scoped>
68
+ .mobile-directory-new {
69
+ padding: 0 20px;
70
+ }
71
+
72
+ .mobile-directory-new__input {
73
+ box-sizing: border-box;
74
+ width: 100%;
75
+ height: 44px;
76
+ border: 1px solid var(--q-input-stroke, var(--q-separator));
77
+ border-radius: 999px;
78
+ padding: 0 16px;
79
+ background: transparent;
80
+ color: var(--q-ink-1);
81
+ font: inherit;
82
+ font-size: 16px;
83
+ }
84
+
85
+ .mobile-directory-new__input:focus {
86
+ outline: none;
87
+ border-color: var(--q-blue-default);
88
+ }
89
+
90
+ .mobile-directory-new__actions {
91
+ display: flex;
92
+ justify-content: flex-end;
93
+ gap: 8px;
94
+ margin-top: 16px;
95
+ }
96
+
97
+ .mobile-directory-new__cancel,
98
+ .mobile-directory-new__create {
99
+ min-height: 40px;
100
+ border-radius: 10px;
101
+ padding: 0 16px;
102
+ font: inherit;
103
+ }
104
+
105
+ .mobile-directory-new__cancel {
106
+ border: 1px solid var(--q-separator);
107
+ background: transparent;
108
+ color: var(--q-ink-2);
109
+ }
110
+
111
+ .mobile-directory-new__create {
112
+ border: 0;
113
+ background: var(--q-blue-default);
114
+ color: var(--q-ink-on-brand);
115
+ }
116
+
117
+ .mobile-directory-new__cancel:disabled,
118
+ .mobile-directory-new__create:disabled {
119
+ opacity: 0.5;
120
+ }
121
+ </style>
@@ -0,0 +1,162 @@
1
+ <template>
2
+ <LaresSheet :open="open" :title="t('desktop.chooseFolder')" @close="$emit('close')">
3
+ <div class="mobile-directory">
4
+ <nav v-if="state.listing" class="mobile-directory__crumbs" :aria-label="t('desktop.folderPath')">
5
+ <button
6
+ v-for="crumb in state.listing.crumbs"
7
+ :key="crumb.path"
8
+ type="button"
9
+ @click="$emit('browse', crumb.path)"
10
+ >
11
+ {{ crumb.path === state.listing.home ? t("desktop.home") : crumb.name }}
12
+ </button>
13
+ </nav>
14
+ <div class="mobile-directory__body">
15
+ <p v-if="state.loading" class="mobile-directory__status">{{ t("desktop.loadingFolders") }}</p>
16
+ <p v-else-if="state.error" class="mobile-directory__error">{{ state.error }}</p>
17
+ <template v-else-if="state.listing">
18
+ <button
19
+ v-for="entry in visibleEntries"
20
+ :key="entry.path"
21
+ type="button"
22
+ class="mobile-directory__row"
23
+ :data-selected="selected === entry.path"
24
+ @click="selected = entry.path"
25
+ >
26
+ <svg viewBox="0 0 24 24" aria-hidden="true"><path d="M3.5 7.5h6l1.6 2H20.5v9H3.5z" /></svg>
27
+ <span>{{ entry.name }}</span>
28
+ <span
29
+ class="mobile-directory__open"
30
+ role="button"
31
+ tabindex="0"
32
+ :aria-label="entry.name"
33
+ @click.stop="$emit('browse', entry.path)"
34
+ @keydown.enter.stop="$emit('browse', entry.path)"
35
+ >
36
+ <svg viewBox="0 0 24 24" aria-hidden="true"><path d="m9 6 6 6-6 6" /></svg>
37
+ </span>
38
+ </button>
39
+ <p v-if="visibleEntries.length === 0" class="mobile-directory__status">{{ t("desktop.noFolders") }}</p>
40
+ </template>
41
+ </div>
42
+ <footer class="mobile-directory__footer">
43
+ <p class="mobile-directory__path">{{ selected || state.listing?.path || "" }}</p>
44
+ <div class="mobile-directory__tools">
45
+ <button
46
+ type="button"
47
+ class="mobile-directory__new-folder"
48
+ :disabled="!state.listing?.path || state.loading || state.creatingFolder"
49
+ @click="newFolderOpen = true"
50
+ >
51
+ <svg viewBox="0 0 24 24" aria-hidden="true"><path d="M12 5v14M5 12h14" /></svg>
52
+ <span>{{ t("desktop.newFolder") }}</span>
53
+ </button>
54
+ <label class="mobile-directory__hidden">
55
+ <input
56
+ type="checkbox"
57
+ :checked="state.showHidden"
58
+ @change="$emit('toggle-hidden', $event.target.checked)"
59
+ />
60
+ <span>{{ t("desktop.showHiddenFiles") }}</span>
61
+ </label>
62
+ </div>
63
+ <div class="mobile-directory__actions">
64
+ <button type="button" class="mobile-directory__cancel" :disabled="state.creating" @click="$emit('close')">
65
+ {{ t("desktop.cancel") }}
66
+ </button>
67
+ <button
68
+ type="button"
69
+ class="mobile-directory__choose"
70
+ :disabled="state.loading || state.creating || !(selected || state.listing?.path)"
71
+ @click="$emit('choose', selected || state.listing.path)"
72
+ >
73
+ {{ state.creating ? t("desktop.addingWorkspace") : t("desktop.addWorkspace") }}
74
+ </button>
75
+ </div>
76
+ </footer>
77
+ </div>
78
+ <LaresMobileDirectoryNewFolder
79
+ :open="newFolderOpen"
80
+ :busy="state.creatingFolder"
81
+ :t="t"
82
+ @close="newFolderOpen = false"
83
+ @create="onCreateFolder"
84
+ />
85
+ </LaresSheet>
86
+ </template>
87
+
88
+ <script>
89
+ import LaresSheet from "../settings/Sheet.vue";
90
+ import LaresMobileDirectoryNewFolder from "./MobileDirectoryNewFolder.vue";
91
+
92
+ export default {
93
+ name: "LaresMobileDirectorySheet",
94
+ components: { LaresSheet, LaresMobileDirectoryNewFolder },
95
+ props: {
96
+ open: { type: Boolean, default: false },
97
+ state: { type: Object, required: true },
98
+ t: { type: Function, required: true },
99
+ },
100
+ emits: ["close", "browse", "choose", "create-folder", "toggle-hidden"],
101
+ data() {
102
+ return { selected: "", newFolderOpen: false };
103
+ },
104
+ computed: {
105
+ visibleEntries() {
106
+ const entries = this.state.listing?.entries ?? [];
107
+ return this.state.showHidden ? entries : entries.filter((entry) => !entry.hidden);
108
+ },
109
+ },
110
+ watch: {
111
+ "state.listing.path"() {
112
+ this.selected = this.state.selectPath || "";
113
+ },
114
+ "state.selectPath"(path) {
115
+ if (path) this.selected = path;
116
+ },
117
+ "state.creatingFolder"(busy, was) {
118
+ if (was && !busy && !this.state.error) this.newFolderOpen = false;
119
+ },
120
+ open(value) {
121
+ if (!value) {
122
+ this.newFolderOpen = false;
123
+ this.selected = "";
124
+ }
125
+ },
126
+ },
127
+ methods: {
128
+ onCreateFolder(name) {
129
+ this.$emit("create-folder", name);
130
+ },
131
+ },
132
+ };
133
+ </script>
134
+
135
+ <style scoped>
136
+ .mobile-directory { display:flex; min-height:min(420px, 55vh); flex-direction:column; margin:-4px 0 0; }
137
+ .mobile-directory svg { width:20px; height:20px; flex-shrink:0; fill:none; stroke:currentColor; stroke-width:1.7; stroke-linecap:round; stroke-linejoin:round; }
138
+ .mobile-directory__crumbs { display:flex; gap:4px; overflow:auto; padding:0 20px 8px; }
139
+ .mobile-directory__crumbs button { flex-shrink:0; border:0; border-radius:8px; padding:6px 8px; background:transparent; color:var(--q-ink-3); font-size:14px; }
140
+ .mobile-directory__body { flex:1; min-height:0; overflow:auto; padding:0 12px; }
141
+ .mobile-directory__row { display:flex; width:100%; align-items:center; gap:10px; border:0; border-radius:12px; padding:12px 10px; background:transparent; color:var(--q-ink-1); text-align:left; }
142
+ .mobile-directory__row[data-selected="true"],
143
+ .mobile-directory__row:active { background:var(--q-background-hover); }
144
+ .mobile-directory__row > span:first-of-type { min-width:0; flex:1; overflow:hidden; text-overflow:ellipsis; white-space:nowrap; }
145
+ .mobile-directory__open { display:grid; width:32px; height:32px; place-items:center; color:var(--q-ink-3); }
146
+ .mobile-directory__status,
147
+ .mobile-directory__error { margin:16px 8px; color:var(--q-ink-3); font-size:15px; text-align:center; }
148
+ .mobile-directory__error { color:var(--q-orange-default); }
149
+ .mobile-directory__footer { display:flex; flex-direction:column; gap:10px; padding:12px 20px calc(12px + env(safe-area-inset-bottom, 0px)); border-top:1px solid var(--q-separator); }
150
+ .mobile-directory__path { width:100%; margin:0; overflow:hidden; color:var(--q-ink-3); font-size:13px; text-overflow:ellipsis; white-space:nowrap; }
151
+ .mobile-directory__tools { display:flex; flex-wrap:wrap; align-items:center; gap:10px 14px; }
152
+ .mobile-directory__new-folder { display:inline-flex; align-items:center; gap:6px; border:1px solid var(--q-separator); border-radius:999px; padding:0 12px; min-height:36px; background:transparent; color:var(--q-ink-2); font:inherit; font-size:14px; }
153
+ .mobile-directory__new-folder svg { width:16px; height:16px; }
154
+ .mobile-directory__hidden { display:inline-flex; align-items:center; gap:8px; color:var(--q-ink-3); font-size:14px; }
155
+ .mobile-directory__hidden input { width:16px; height:16px; margin:0; }
156
+ .mobile-directory__actions { display:flex; justify-content:flex-end; gap:8px; }
157
+ .mobile-directory__cancel,
158
+ .mobile-directory__choose { min-height:40px; border-radius:10px; padding:0 14px; font:inherit; }
159
+ .mobile-directory__cancel { border:1px solid var(--q-separator); background:transparent; color:var(--q-ink-2); }
160
+ .mobile-directory__choose { border:0; background:var(--q-blue-default); color:var(--q-ink-on-brand); }
161
+ .mobile-directory button:disabled { opacity:.5; }
162
+ </style>
@@ -0,0 +1,217 @@
1
+ <template>
2
+ <section class="mobile-new-session" :aria-label="t('desktop.newSession')">
3
+ <div class="mobile-new-session__brand">
4
+ <div class="mobile-new-session__logo" :style="logoStyle" aria-hidden="true" />
5
+ <h2 class="mobile-new-session__title">{{ t("shell.title") }}</h2>
6
+ </div>
7
+
8
+ <div class="mobile-new-session__prompt">
9
+ <div class="mobile-new-session__selectors">
10
+ <button
11
+ type="button"
12
+ class="mobile-new-session__chip mobile-new-session__chip--workspace"
13
+ :disabled="workspaceBusy"
14
+ :aria-label="t('desktop.workspaces')"
15
+ @click="$emit('workspace')"
16
+ >
17
+ <svg viewBox="0 0 24 24" aria-hidden="true">
18
+ <path d="M3.5 7.5h6l1.6 2H20.5v9H3.5z" />
19
+ </svg>
20
+ <span>{{ workspaceLabel }}</span>
21
+ <svg class="mobile-new-session__chevron" viewBox="0 0 24 24" aria-hidden="true">
22
+ <path d="M7 10l5 5 5-5" />
23
+ </svg>
24
+ </button>
25
+ <button
26
+ type="button"
27
+ class="mobile-new-session__chip mobile-new-session__chip--model"
28
+ :disabled="modelBusy"
29
+ :aria-label="modelAria"
30
+ @click="$emit('model')"
31
+ >
32
+ <span>{{ modelLabel || t("model.select") }}</span>
33
+ <svg class="mobile-new-session__chevron" viewBox="0 0 24 24" aria-hidden="true">
34
+ <path d="M7 10l5 5 5-5" />
35
+ </svg>
36
+ </button>
37
+ </div>
38
+ <div class="mobile-new-session__composer">
39
+ <slot />
40
+ </div>
41
+ </div>
42
+ </section>
43
+ </template>
44
+
45
+ <script>
46
+ import { MARK_DATA_URI } from "@olares/lares-core/icons/mark";
47
+
48
+ export default {
49
+ name: "LaresMobileNewSession",
50
+ props: {
51
+ t: { type: Function, required: true },
52
+ workspaceLabel: { type: String, default: "" },
53
+ workspaceBusy: { type: Boolean, default: false },
54
+ modelLabel: { type: String, default: "" },
55
+ modelBusy: { type: Boolean, default: false },
56
+ },
57
+ emits: ["workspace", "model"],
58
+ computed: {
59
+ logoStyle() {
60
+ return { backgroundImage: MARK_DATA_URI };
61
+ },
62
+ modelAria() {
63
+ return this.modelLabel
64
+ ? this.t("model.switchAria", { label: this.modelLabel })
65
+ : this.t("model.select");
66
+ },
67
+ },
68
+ };
69
+ </script>
70
+
71
+ <style scoped>
72
+ .mobile-new-session {
73
+ display: flex;
74
+ width: 100%;
75
+ min-height: 0;
76
+ flex: 1;
77
+ flex-direction: column;
78
+ align-items: center;
79
+ justify-content: center;
80
+ padding: 20px 20px calc(12px + env(safe-area-inset-bottom, 0px));
81
+ }
82
+
83
+ .mobile-new-session__brand {
84
+ display: flex;
85
+ flex-direction: column;
86
+ align-items: center;
87
+ gap: 8px;
88
+ margin-bottom: 32px;
89
+ }
90
+
91
+ .mobile-new-session__logo {
92
+ width: 56px;
93
+ height: 56px;
94
+ border-radius: 14px;
95
+ background-position: center;
96
+ background-repeat: no-repeat;
97
+ background-size: cover;
98
+ box-shadow: inset 0 1.8px 0 rgb(255 255 255 / 50%);
99
+ animation: mobile-brand-in var(--lares-duration-normal, 220ms) var(--lares-ease-out, cubic-bezier(0.32, 0.72, 0, 1));
100
+ }
101
+
102
+ @keyframes mobile-brand-in {
103
+ from {
104
+ opacity: 0;
105
+ transform: scale(0.92);
106
+ }
107
+ }
108
+
109
+ @media (prefers-reduced-motion: reduce) {
110
+ .mobile-new-session__logo {
111
+ animation: none;
112
+ }
113
+ }
114
+
115
+ .mobile-new-session__title {
116
+ margin: 0;
117
+ font-size: 28px;
118
+ font-weight: 600;
119
+ line-height: 34px;
120
+ letter-spacing: -0.02em;
121
+ }
122
+
123
+ .mobile-new-session__prompt {
124
+ display: flex;
125
+ width: min(100%, 420px);
126
+ flex-direction: column;
127
+ gap: 6px;
128
+ }
129
+
130
+ .mobile-new-session__selectors {
131
+ display: flex;
132
+ width: 100%;
133
+ align-items: center;
134
+ justify-content: space-between;
135
+ gap: 10px;
136
+ }
137
+
138
+ .mobile-new-session__chip {
139
+ display: inline-flex;
140
+ min-width: 0;
141
+ align-items: center;
142
+ gap: 6px;
143
+ border: 0;
144
+ border-radius: 999px;
145
+ padding: 6px 10px;
146
+ background: transparent;
147
+ color: var(--q-ink-2);
148
+ font-size: 14px;
149
+ font-weight: 500;
150
+ line-height: 18px;
151
+ transition: background var(--lares-duration-fast, 120ms) var(--lares-ease-out, ease),
152
+ color var(--lares-duration-fast, 120ms) var(--lares-ease-out, ease);
153
+ }
154
+
155
+ .mobile-new-session__chip--workspace {
156
+ flex: 0 1 auto;
157
+ max-width: 46%;
158
+ }
159
+
160
+ .mobile-new-session__chip--model {
161
+ flex: 1 1 auto;
162
+ justify-content: flex-end;
163
+ max-width: 54%;
164
+ }
165
+
166
+ .mobile-new-session__chip span {
167
+ min-width: 0;
168
+ overflow: hidden;
169
+ text-overflow: ellipsis;
170
+ white-space: nowrap;
171
+ }
172
+
173
+ .mobile-new-session__chip svg:not(.mobile-new-session__chevron) {
174
+ width: 18px;
175
+ height: 18px;
176
+ flex-shrink: 0;
177
+ fill: none;
178
+ stroke: currentColor;
179
+ stroke-width: 1.75;
180
+ stroke-linejoin: round;
181
+ }
182
+
183
+ .mobile-new-session__chevron {
184
+ width: 16px;
185
+ height: 16px;
186
+ flex-shrink: 0;
187
+ fill: none;
188
+ stroke: currentColor;
189
+ stroke-width: 1.75;
190
+ stroke-linecap: round;
191
+ stroke-linejoin: round;
192
+ }
193
+
194
+ .mobile-new-session__chip:active:not(:disabled) {
195
+ background: var(--q-btn-bg-pressed);
196
+ }
197
+
198
+ .mobile-new-session__composer {
199
+ width: 100%;
200
+ }
201
+
202
+ @media (max-height: 640px) {
203
+ .mobile-new-session__brand {
204
+ margin-bottom: 24px;
205
+ }
206
+
207
+ .mobile-new-session__logo {
208
+ width: 48px;
209
+ height: 48px;
210
+ }
211
+
212
+ .mobile-new-session__title {
213
+ font-size: 24px;
214
+ line-height: 30px;
215
+ }
216
+ }
217
+ </style>