@olares/lares-mobile 0.15.0

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.
@@ -0,0 +1,227 @@
1
+ <template>
2
+ <div
3
+ class="lares-history"
4
+ :data-open="open ? 'true' : 'false'"
5
+ :inert="!open"
6
+ :aria-hidden="!open"
7
+ >
8
+ <div class="lares-history__backdrop" @click="$emit('close')" />
9
+ <aside class="lares-history__sheet" role="dialog" :aria-label="t('bar.history')">
10
+ <header class="lares-history__head">
11
+ <h2 class="lares-history__title">{{ t("bar.history") }}</h2>
12
+ </header>
13
+ <div class="lares-history__list">
14
+ <div
15
+ v-if="pending"
16
+ class="lares-history__skeleton"
17
+ role="status"
18
+ :aria-label="t('history.loading')"
19
+ >
20
+ <section v-for="block in [1, 2]" :key="block" class="lares-history__section">
21
+ <span class="lares-history__bone lares-history__bone--label" />
22
+ <span class="lares-history__bone" />
23
+ <span class="lares-history__bone" />
24
+ <span class="lares-history__bone" />
25
+ <span class="lares-history__bone" />
26
+ </section>
27
+ </div>
28
+ <p v-else-if="sections.length === 0" class="lares-history__empty">{{ t("history.empty") }}</p>
29
+ <section v-for="section in sections" :key="section.id" class="lares-history__section">
30
+ <h3 class="lares-history__label">{{ t(section.key) }}</h3>
31
+ <button
32
+ v-for="row in section.rows"
33
+ :key="row.sessionId"
34
+ type="button"
35
+ class="lares-history__item"
36
+ :data-current="row.sessionId === sessionId ? 'true' : 'false'"
37
+ @click="$emit('pick', row.sessionId)"
38
+ >
39
+ {{ row.title || t("history.untitled") }}
40
+ </button>
41
+ </section>
42
+ </div>
43
+ </aside>
44
+ </div>
45
+ </template>
46
+
47
+ <script>
48
+ import { groupSessionsByRecency, visibleHistorySessions } from "@olares/lares-core/larepass/chat";
49
+
50
+ export default {
51
+ name: "LaresHistoryPanel",
52
+ props: {
53
+ open: { type: Boolean, default: false },
54
+ sessions: { type: Array, default: () => [] },
55
+ sessionId: { type: String, default: "" },
56
+ ready: { type: Boolean, default: false },
57
+ t: { type: Function, required: true },
58
+ },
59
+ emits: ["close", "pick"],
60
+ computed: {
61
+ pending() {
62
+ return !this.ready && this.sections.length === 0;
63
+ },
64
+ sections() {
65
+ return groupSessionsByRecency(visibleHistorySessions(this.sessions, this.sessionId));
66
+ },
67
+ },
68
+ };
69
+ </script>
70
+
71
+ <style scoped>
72
+ .lares-history {
73
+ position: absolute;
74
+ inset: 0;
75
+ z-index: 4;
76
+ pointer-events: none;
77
+ }
78
+
79
+ .lares-history[data-open="true"] {
80
+ pointer-events: auto;
81
+ }
82
+
83
+ .lares-history__backdrop {
84
+ position: absolute;
85
+ inset: 0;
86
+ background: var(--q-background-alpha);
87
+ opacity: 0;
88
+ transition: opacity 0.28s cubic-bezier(0.32, 0.72, 0, 1);
89
+ }
90
+
91
+ .lares-history[data-open="true"] .lares-history__backdrop {
92
+ opacity: 1;
93
+ }
94
+
95
+ .lares-history__sheet {
96
+ position: absolute;
97
+ top: 0;
98
+ bottom: 0;
99
+ left: 0;
100
+ display: flex;
101
+ width: min(80vw, 320px);
102
+ height: 100%;
103
+ flex-direction: column;
104
+ min-height: 0;
105
+ background: var(--q-background-1);
106
+ box-shadow: 8px 0 32px rgb(0 0 0 / 8%);
107
+ transform: translateX(-100%);
108
+ transition: transform 0.28s cubic-bezier(0.32, 0.72, 0, 1);
109
+ }
110
+
111
+ .lares-history[data-open="true"] .lares-history__sheet {
112
+ transform: translateX(0);
113
+ }
114
+
115
+ .lares-history__head {
116
+ display: flex;
117
+ align-items: center;
118
+ flex-shrink: 0;
119
+ height: 56px;
120
+ padding: 0 20px;
121
+ }
122
+
123
+ .lares-history__title {
124
+ margin: 0;
125
+ font-size: 18px;
126
+ font-weight: 500;
127
+ line-height: 24px;
128
+ }
129
+
130
+ .lares-history__list {
131
+ display: flex;
132
+ flex: 1;
133
+ min-height: 0;
134
+ flex-direction: column;
135
+ gap: 16px;
136
+ overflow-x: hidden;
137
+ overflow-y: auto;
138
+ padding: 4px 12px 24px;
139
+ overscroll-behavior: contain;
140
+ -webkit-overflow-scrolling: touch;
141
+ }
142
+
143
+ .lares-history__section {
144
+ display: flex;
145
+ flex-direction: column;
146
+ gap: 4px;
147
+ }
148
+
149
+ .lares-history__label {
150
+ margin: 0;
151
+ padding: 8px 16px 4px;
152
+ color: var(--q-ink-3);
153
+ font-size: 12px;
154
+ font-weight: 500;
155
+ line-height: 16px;
156
+ }
157
+
158
+ .lares-history__empty {
159
+ margin: 12px 8px;
160
+ font-size: 13px;
161
+ color: var(--q-ink-3);
162
+ }
163
+
164
+ .lares-history__skeleton {
165
+ display: flex;
166
+ flex-direction: column;
167
+ gap: 16px;
168
+ }
169
+
170
+ .lares-history__bone {
171
+ display: block;
172
+ height: 44px;
173
+ border-radius: 12px;
174
+ background: var(--q-background-3);
175
+ animation: lares-history-pulse 1.2s ease-in-out infinite;
176
+ }
177
+
178
+ .lares-history__bone--label {
179
+ width: 72px;
180
+ height: 12px;
181
+ margin: 8px 16px 4px;
182
+ border-radius: 6px;
183
+ }
184
+
185
+ .lares-history__section .lares-history__bone:nth-child(2) { width: 88%; }
186
+ .lares-history__section .lares-history__bone:nth-child(3) { width: 72%; }
187
+ .lares-history__section .lares-history__bone:nth-child(4) { width: 80%; }
188
+ .lares-history__section .lares-history__bone:nth-child(5) { width: 64%; }
189
+
190
+ @keyframes lares-history-pulse {
191
+ 50% { opacity: 0.45; }
192
+ }
193
+
194
+ @media (prefers-reduced-motion: reduce) {
195
+ .lares-history__bone {
196
+ animation: none;
197
+ }
198
+ }
199
+
200
+ .lares-history__item {
201
+ width: 100%;
202
+ overflow: hidden;
203
+ border: 0;
204
+ border-radius: 12px;
205
+ padding: 12px 16px;
206
+ text-align: left;
207
+ background: transparent;
208
+ color: var(--q-ink-1);
209
+ text-overflow: ellipsis;
210
+ white-space: nowrap;
211
+ }
212
+
213
+ .lares-history__item[data-current="true"] {
214
+ background: var(--q-background-3);
215
+ }
216
+
217
+ .lares-history__item:active {
218
+ background: var(--q-background-hover);
219
+ }
220
+
221
+ @media (prefers-reduced-motion: reduce) {
222
+ .lares-history__backdrop,
223
+ .lares-history__sheet {
224
+ transition: none;
225
+ }
226
+ }
227
+ </style>
@@ -0,0 +1,129 @@
1
+ <template>
2
+ <div class="lares-actions" role="toolbar" :aria-label="t('action.bar')">
3
+ <div class="lares-actions__left">
4
+ <button type="button" :aria-label="copied ? t('action.copied') : t('action.copy')" @click="copy">
5
+ <svg viewBox="0 0 24 24" aria-hidden="true">
6
+ <rect x="9" y="9" width="11" height="13" rx="2" />
7
+ <path d="M6 16H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1" />
8
+ </svg>
9
+ </button>
10
+ <button
11
+ type="button"
12
+ :aria-label="t('action.like')"
13
+ :aria-pressed="reaction === 'like' ? 'true' : 'false'"
14
+ :data-on="reaction === 'like' ? 'true' : 'false'"
15
+ @click="$emit('react', reaction === 'like' ? '' : 'like')"
16
+ >
17
+ <svg viewBox="0 0 24 24" aria-hidden="true">
18
+ <path d="M7 11v10H4a1 1 0 0 1-1-1v-8a1 1 0 0 1 1-1h3Zm0 0 4.2-8.1A2.2 2.2 0 0 1 15.2 4v5H20a2 2 0 0 1 2 2.3l-1.2 8A2 2 0 0 1 18.8 21H7" />
19
+ </svg>
20
+ </button>
21
+ <button
22
+ type="button"
23
+ :aria-label="t('action.dislike')"
24
+ :aria-pressed="reaction === 'dislike' ? 'true' : 'false'"
25
+ :data-on="reaction === 'dislike' ? 'true' : 'false'"
26
+ @click="$emit('react', reaction === 'dislike' ? '' : 'dislike')"
27
+ >
28
+ <svg viewBox="0 0 24 24" aria-hidden="true">
29
+ <path d="M17 13V3h3a1 1 0 0 1 1 1v8a1 1 0 0 1-1 1h-3Zm0 0-4.2 8.1A2.2 2.2 0 0 1 8.8 20v-5H4a2 2 0 0 1-2-2.3l1.2-8A2 2 0 0 1 5.2 3H17" />
30
+ </svg>
31
+ </button>
32
+ <button type="button" :aria-label="t('action.share')" @click="share">
33
+ <svg viewBox="0 0 24 24" aria-hidden="true">
34
+ <path d="M4 20v-7a4 4 0 0 1 4-4h12" />
35
+ <path d="m16 5 4 4-4 4" />
36
+ </svg>
37
+ </button>
38
+ </div>
39
+ </div>
40
+ </template>
41
+
42
+ <script>
43
+ export default {
44
+ name: "LaresMessageActions",
45
+ props: {
46
+ text: { type: String, default: "" },
47
+ reaction: { type: String, default: "" },
48
+ t: { type: Function, required: true },
49
+ },
50
+ emits: ["react"],
51
+ data() {
52
+ return { copied: false, copyTimer: 0 };
53
+ },
54
+ beforeUnmount() {
55
+ if (this.copyTimer) clearTimeout(this.copyTimer);
56
+ },
57
+ methods: {
58
+ async copy() {
59
+ try {
60
+ await navigator.clipboard.writeText(this.text);
61
+ this.copied = true;
62
+ if (this.copyTimer) clearTimeout(this.copyTimer);
63
+ this.copyTimer = setTimeout(() => {
64
+ this.copied = false;
65
+ }, 1600);
66
+ } catch {
67
+ // clipboard may be blocked
68
+ }
69
+ },
70
+ async share() {
71
+ if (typeof navigator.share === "function") {
72
+ try {
73
+ await navigator.share({ text: this.text });
74
+ return;
75
+ } catch (err) {
76
+ if (err?.name === "AbortError") return;
77
+ }
78
+ }
79
+ await this.copy();
80
+ },
81
+ },
82
+ };
83
+ </script>
84
+
85
+ <style scoped>
86
+ .lares-actions {
87
+ display: flex;
88
+ align-items: center;
89
+ gap: 8px;
90
+ margin-top: 2px;
91
+ }
92
+
93
+ .lares-actions__left {
94
+ display: flex;
95
+ align-items: center;
96
+ gap: 2px;
97
+ }
98
+
99
+ .lares-actions button {
100
+ display: inline-flex;
101
+ align-items: center;
102
+ justify-content: center;
103
+ width: 32px;
104
+ height: 32px;
105
+ border: 0;
106
+ border-radius: 8px;
107
+ padding: 0;
108
+ background: transparent;
109
+ color: var(--q-ink-3);
110
+ }
111
+
112
+ .lares-actions button[data-on="true"] {
113
+ color: var(--q-blue-default);
114
+ }
115
+
116
+ .lares-actions button:active {
117
+ background: var(--q-btn-bg-pressed);
118
+ }
119
+
120
+ .lares-actions svg {
121
+ width: 18px;
122
+ height: 18px;
123
+ fill: none;
124
+ stroke: currentColor;
125
+ stroke-width: 1.7;
126
+ stroke-linecap: round;
127
+ stroke-linejoin: round;
128
+ }
129
+ </style>
@@ -0,0 +1,126 @@
1
+ <template>
2
+ <section class="lares-question" :aria-label="heading">
3
+ <p class="lares-question__heading">{{ heading }}</p>
4
+ <p class="lares-question__prompt">{{ prompt }}</p>
5
+ <p v-if="detail" class="lares-question__detail">{{ detail }}</p>
6
+ <div class="lares-question__options">
7
+ <button
8
+ v-for="option in options"
9
+ :key="option.label"
10
+ type="button"
11
+ class="lares-question__option"
12
+ :disabled="disabled"
13
+ @click="$emit('pick', option.label)"
14
+ >
15
+ <span class="lares-question__label">{{ option.label }}</span>
16
+ <span v-if="option.description" class="lares-question__hint">{{ option.description }}</span>
17
+ </button>
18
+ </div>
19
+ <p v-if="hint" class="lares-question__wait">{{ hint }}</p>
20
+ </section>
21
+ </template>
22
+
23
+ <script>
24
+ export default {
25
+ name: "LaresQuestionCard",
26
+ props: {
27
+ questions: { type: Array, default: () => [] },
28
+ rpcId: { type: String, default: "" },
29
+ busy: { type: Boolean, default: false },
30
+ t: { type: Function, required: true },
31
+ },
32
+ emits: ["pick"],
33
+ computed: {
34
+ current() {
35
+ return this.questions[0] || null;
36
+ },
37
+ heading() {
38
+ return this.current?.header || this.t("question.title");
39
+ },
40
+ prompt() {
41
+ return this.current?.question || "";
42
+ },
43
+ detail() {
44
+ return this.current?.detail || "";
45
+ },
46
+ options() {
47
+ return Array.isArray(this.current?.options) ? this.current.options : [];
48
+ },
49
+ disabled() {
50
+ return this.busy || !this.rpcId || !this.current;
51
+ },
52
+ hint() {
53
+ if (this.busy) return this.t("question.busy");
54
+ if (!this.rpcId) return this.t("question.waiting");
55
+ return "";
56
+ },
57
+ },
58
+ };
59
+ </script>
60
+
61
+ <style scoped>
62
+ .lares-question {
63
+ display: flex;
64
+ min-width: 0;
65
+ flex-direction: column;
66
+ gap: 10px;
67
+ margin: 8px 0;
68
+ padding: 12px;
69
+ border: 1px solid var(--q-separator);
70
+ border-radius: 14px;
71
+ background: var(--q-background-1);
72
+ }
73
+
74
+ .lares-question__heading {
75
+ margin: 0;
76
+ color: var(--q-ink-2);
77
+ font-size: 12px;
78
+ }
79
+
80
+ .lares-question__prompt {
81
+ margin: 0;
82
+ color: var(--q-ink-1);
83
+ font-size: 15px;
84
+ font-weight: 500;
85
+ }
86
+
87
+ .lares-question__detail,
88
+ .lares-question__wait {
89
+ margin: 0;
90
+ color: var(--q-ink-3);
91
+ font-size: 13px;
92
+ }
93
+
94
+ .lares-question__options {
95
+ display: flex;
96
+ flex-direction: column;
97
+ gap: 8px;
98
+ }
99
+
100
+ .lares-question__option {
101
+ display: flex;
102
+ min-width: 0;
103
+ flex-direction: column;
104
+ gap: 2px;
105
+ padding: 10px 12px;
106
+ border: 1px solid var(--q-separator);
107
+ border-radius: 10px;
108
+ background: var(--q-background-hover);
109
+ color: inherit;
110
+ text-align: left;
111
+ }
112
+
113
+ .lares-question__option:disabled {
114
+ opacity: 0.55;
115
+ }
116
+
117
+ .lares-question__label {
118
+ font-size: 14px;
119
+ color: var(--q-ink-1);
120
+ }
121
+
122
+ .lares-question__hint {
123
+ font-size: 12px;
124
+ color: var(--q-ink-3);
125
+ }
126
+ </style>
@@ -0,0 +1,43 @@
1
+ <template>
2
+ <svg
3
+ class="lares-stage-icon"
4
+ :viewBox="glyph.viewBox"
5
+ :width="14"
6
+ :height="14"
7
+ aria-hidden="true"
8
+ fill="none"
9
+ >
10
+ <path
11
+ v-for="(p, i) in glyph.paths"
12
+ :key="i"
13
+ :d="p.d"
14
+ :fill-rule="p.fillRule"
15
+ :clip-rule="p.clipRule"
16
+ :transform="p.transform"
17
+ fill="currentColor"
18
+ />
19
+ </svg>
20
+ </template>
21
+
22
+ <script>
23
+ import { GLYPHS } from "./stage-icons.js";
24
+
25
+ export default {
26
+ name: "LaresStageIcon",
27
+ props: {
28
+ name: { type: String, required: true },
29
+ },
30
+ computed: {
31
+ glyph() {
32
+ return GLYPHS[this.name] || GLYPHS.sparkle;
33
+ },
34
+ },
35
+ };
36
+ </script>
37
+
38
+ <style scoped>
39
+ .lares-stage-icon {
40
+ display: block;
41
+ flex: none;
42
+ }
43
+ </style>
@@ -0,0 +1,131 @@
1
+ <template>
2
+ <div class="lares-stage" :data-kind="kind" :data-open="open ? 'true' : 'false'">
3
+ <button type="button" class="lares-stage__row" :aria-expanded="open" @click="open = !open">
4
+ <span class="lares-stage__leading" aria-hidden="true">
5
+ <LaresStageIcon :name="iconName" />
6
+ </span>
7
+ <span class="lares-stage__title">{{ title }}</span>
8
+ <template v-if="summary">
9
+ <span class="lares-stage__sep" aria-hidden="true" />
10
+ <span class="lares-stage__summary">{{ summary }}</span>
11
+ </template>
12
+ </button>
13
+ <pre v-if="open && body" class="lares-stage__body">{{ body }}</pre>
14
+ </div>
15
+ </template>
16
+
17
+ <script>
18
+ import { stageIconOf } from "./stage-icons.js";
19
+ import LaresStageIcon from "./StageIcon.vue";
20
+
21
+ export default {
22
+ name: "LaresStageRow",
23
+ components: { LaresStageIcon },
24
+ props: {
25
+ kind: { type: String, default: "" },
26
+ variant: { type: String, default: "" },
27
+ title: { type: String, required: true },
28
+ summary: { type: String, default: "" },
29
+ body: { type: String, default: "" },
30
+ },
31
+ data() {
32
+ return { open: false };
33
+ },
34
+ computed: {
35
+ iconName() {
36
+ return stageIconOf(this.kind, this.variant);
37
+ },
38
+ },
39
+ };
40
+ </script>
41
+
42
+ <style scoped>
43
+ .lares-stage {
44
+ min-width: 0;
45
+ max-width: 100%;
46
+ }
47
+
48
+ .lares-stage__row {
49
+ display: flex;
50
+ align-items: center;
51
+ width: 100%;
52
+ min-width: 0;
53
+ min-height: 24px;
54
+ border: 0;
55
+ padding: 0;
56
+ background: transparent;
57
+ text-align: left;
58
+ cursor: pointer;
59
+ }
60
+
61
+ .lares-stage__leading {
62
+ display: inline-flex;
63
+ flex: none;
64
+ align-items: center;
65
+ justify-content: center;
66
+ width: 16px;
67
+ height: 16px;
68
+ margin-right: 6px;
69
+ color: var(--q-ink-2);
70
+ }
71
+
72
+ .lares-stage__title {
73
+ flex: none;
74
+ color: var(--q-ink-2);
75
+ }
76
+
77
+ .lares-stage__title,
78
+ .lares-stage__summary {
79
+ min-width: 0;
80
+ overflow: hidden;
81
+ font-size: 14px;
82
+ line-height: 24px;
83
+ text-overflow: ellipsis;
84
+ white-space: nowrap;
85
+ }
86
+
87
+ .lares-stage[data-kind="retry"] .lares-stage__title {
88
+ flex: auto;
89
+ }
90
+
91
+ .lares-stage__summary {
92
+ flex: auto;
93
+ color: var(--q-ink-2);
94
+ }
95
+
96
+ .lares-stage__sep {
97
+ flex: none;
98
+ width: 2px;
99
+ height: 2px;
100
+ margin: 0 8px;
101
+ border-radius: 1px;
102
+ background: var(--q-ink-2);
103
+ }
104
+
105
+ .lares-stage__body {
106
+ box-sizing: border-box;
107
+ min-width: 0;
108
+ max-width: 100%;
109
+ max-height: 141px;
110
+ margin: 4px 0 0 22px;
111
+ border-radius: 8px;
112
+ padding: 10px 12px;
113
+ overflow: auto;
114
+ background: var(--q-background-3);
115
+ color: var(--q-ink-2);
116
+ font: 400 11px/16px ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, monospace;
117
+ white-space: pre-wrap;
118
+ overflow-wrap: anywhere;
119
+ word-break: break-word;
120
+ }
121
+
122
+ .lares-stage[data-kind="think"] .lares-stage__body {
123
+ max-height: none;
124
+ margin: 6px 0 2px 2px;
125
+ border-radius: 0;
126
+ border-left: 2px solid var(--q-input-stroke);
127
+ padding: 0 0 0 12px;
128
+ background: transparent;
129
+ font: 400 13px/1.6 inherit;
130
+ }
131
+ </style>