@www.hyperlinks.space/program-kit 18.18.18 → 123.123.123
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.
- package/.eas/workflows/create-development-builds.yml +21 -0
- package/.eas/workflows/create-draft.yml +15 -0
- package/.eas/workflows/deploy-to-production.yml +68 -0
- package/.gitattributes +48 -0
- package/.gitignore +52 -0
- package/.nvmrc +1 -0
- package/.vercelignore +6 -0
- package/README.md +17 -2
- package/ai/openai.ts +202 -0
- package/ai/transmitter.ts +367 -0
- package/backlogs/medium_term_backlog.md +26 -0
- package/backlogs/short_term_backlog.md +42 -0
- package/eslint.config.cjs +10 -0
- package/npmReadMe.md +17 -2
- package/npmrc.example +1 -0
- package/package.json +3 -28
- package/polyfills/buffer.ts +9 -0
- package/research & docs/ai_and_search_bar_input.md +94 -0
- package/research & docs/ai_bot_messages.md +124 -0
- package/research & docs/auth-and-centralized-encrypted-keys-plan.md +440 -0
- package/research & docs/blue_bar_tackling.md +143 -0
- package/research & docs/bot_async_streaming.md +174 -0
- package/research & docs/build_and_install.md +129 -0
- package/research & docs/database_messages.md +34 -0
- package/research & docs/fonts.md +18 -0
- package/research & docs/github-gitlab-bidirectional-mirroring.md +154 -0
- package/research & docs/keys-retrieval-console-scripts.js +131 -0
- package/research & docs/npm-release.md +46 -0
- package/research & docs/releases.md +201 -0
- package/research & docs/releases_github_actions.md +188 -0
- package/research & docs/scalability.md +34 -0
- package/research & docs/security_plan_raw.md +244 -0
- package/research & docs/security_raw.md +354 -0
- package/research & docs/storage-availability-console-script.js +152 -0
- package/research & docs/storage-lifetime.md +33 -0
- package/research & docs/telegram-raw-keys-cloud-storage-risks.md +31 -0
- package/research & docs/timing_raw.md +63 -0
- package/research & docs/tma_logo_bar_jump_investigation.md +69 -0
- package/research & docs/update.md +205 -0
- package/research & docs/wallet_telegram_standalone_multichain_proposal.md +192 -0
- package/research & docs/wallets_hosting_architecture.md +403 -0
- package/services/wallet/tonWallet.ts +73 -0
- package/ui/components/GlobalBottomBar.tsx +447 -0
- package/ui/components/GlobalBottomBarWeb.tsx +362 -0
- package/ui/components/GlobalLogoBar.tsx +108 -0
- package/ui/components/GlobalLogoBarFallback.tsx +66 -0
- package/ui/components/GlobalLogoBarWithFallback.tsx +24 -0
- package/ui/components/HyperlinksSpaceLogo.tsx +29 -0
- package/ui/components/Telegram.tsx +677 -0
- package/ui/components/telegramWebApp.ts +359 -0
- package/ui/fonts.ts +12 -0
- package/ui/theme.ts +117 -0
|
@@ -0,0 +1,367 @@
|
|
|
1
|
+
import type { AiMode, AiRequestBase, AiResponseBase, ThreadContext } from "./openai.js";
|
|
2
|
+
import { callOpenAiChat, callOpenAiChatStream } from "./openai.js";
|
|
3
|
+
import {
|
|
4
|
+
getTokenBySymbol,
|
|
5
|
+
normalizeSymbol,
|
|
6
|
+
type TokenSearchResult,
|
|
7
|
+
} from "../blockchain/coffee.js";
|
|
8
|
+
import {
|
|
9
|
+
insertMessage,
|
|
10
|
+
getThreadHistory,
|
|
11
|
+
type Message,
|
|
12
|
+
} from "../database/messages.js";
|
|
13
|
+
|
|
14
|
+
export type AiRequest = AiRequestBase & {
|
|
15
|
+
mode?: AiMode;
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
export type AiResponse = AiResponseBase;
|
|
19
|
+
|
|
20
|
+
const HISTORY_LIMIT = 50;
|
|
21
|
+
|
|
22
|
+
function formatHistoryForInput(history: Message[]): string {
|
|
23
|
+
if (history.length === 0) return "";
|
|
24
|
+
const lines = history.map((m) => {
|
|
25
|
+
const role = m.role === "user" ? "user" : m.role === "assistant" ? "assistant" : "system";
|
|
26
|
+
const content = (m.content ?? "").trim();
|
|
27
|
+
return `${role}: ${content}`;
|
|
28
|
+
});
|
|
29
|
+
return "Previous conversation:\n" + lines.join("\n") + "\n\n";
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/** Claim by insert; return skipped response if another instance won. */
|
|
33
|
+
async function claimUserMessage(
|
|
34
|
+
thread: ThreadContext,
|
|
35
|
+
content: string,
|
|
36
|
+
): Promise<AiResponse | null> {
|
|
37
|
+
const inserted = await insertMessage({
|
|
38
|
+
user_telegram: thread.user_telegram,
|
|
39
|
+
thread_id: thread.thread_id,
|
|
40
|
+
type: thread.type,
|
|
41
|
+
role: "user",
|
|
42
|
+
content,
|
|
43
|
+
telegram_update_id: thread.telegram_update_id ?? undefined,
|
|
44
|
+
});
|
|
45
|
+
if (inserted === null) {
|
|
46
|
+
return {
|
|
47
|
+
ok: false,
|
|
48
|
+
provider: "openai",
|
|
49
|
+
mode: "chat",
|
|
50
|
+
skipped: true,
|
|
51
|
+
};
|
|
52
|
+
}
|
|
53
|
+
return null;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
async function persistAssistantMessage(
|
|
57
|
+
thread: ThreadContext,
|
|
58
|
+
content: string,
|
|
59
|
+
): Promise<void> {
|
|
60
|
+
await insertMessage({
|
|
61
|
+
user_telegram: thread.user_telegram,
|
|
62
|
+
thread_id: thread.thread_id,
|
|
63
|
+
type: thread.type,
|
|
64
|
+
role: "assistant",
|
|
65
|
+
content,
|
|
66
|
+
});
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
function extractSymbolCandidate(input: string): string | null {
|
|
70
|
+
const raw = input.trim();
|
|
71
|
+
if (!raw) return null;
|
|
72
|
+
|
|
73
|
+
// Simple patterns like "USDT", "$USDT", "USDT on TON".
|
|
74
|
+
const parts = raw.split(/\s+/);
|
|
75
|
+
const first = parts[0]?.replace(/^\$/g, "") ?? "";
|
|
76
|
+
const normalized = normalizeSymbol(first);
|
|
77
|
+
return normalized || null;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
function buildTokenFactsBlock(symbol: string, token: any): string {
|
|
81
|
+
const lines: string[] = [];
|
|
82
|
+
|
|
83
|
+
const sym = token?.symbol ?? symbol;
|
|
84
|
+
const name = token?.name ?? null;
|
|
85
|
+
const address = token?.id ?? token?.address ?? null;
|
|
86
|
+
const type = token?.type ?? "token";
|
|
87
|
+
const decimals = token?.decimals ?? token?.metadata?.decimals ?? null;
|
|
88
|
+
const verification =
|
|
89
|
+
token?.verification ?? token?.metadata?.verification ?? null;
|
|
90
|
+
|
|
91
|
+
const market = token?.market_stats ?? {};
|
|
92
|
+
const holders =
|
|
93
|
+
market?.holders_count ?? token?.holders ?? market?.holders ?? null;
|
|
94
|
+
const priceUsd = market?.price_usd ?? null;
|
|
95
|
+
const mcap = market?.mcap ?? market?.fdmc ?? null;
|
|
96
|
+
const volume24h = market?.volume_usd_24h ?? null;
|
|
97
|
+
|
|
98
|
+
lines.push(`Symbol: ${sym}`);
|
|
99
|
+
if (name) {
|
|
100
|
+
lines.push(`Name: ${name}`);
|
|
101
|
+
}
|
|
102
|
+
lines.push(`Type: ${type}`);
|
|
103
|
+
lines.push(`Blockchain: TON`);
|
|
104
|
+
if (address) {
|
|
105
|
+
lines.push(`Address: ${address}`);
|
|
106
|
+
}
|
|
107
|
+
if (decimals != null) {
|
|
108
|
+
lines.push(`Decimals: ${decimals}`);
|
|
109
|
+
}
|
|
110
|
+
if (verification) {
|
|
111
|
+
lines.push(`Verification: ${verification}`);
|
|
112
|
+
}
|
|
113
|
+
if (holders != null) {
|
|
114
|
+
lines.push(`Holders: ${holders}`);
|
|
115
|
+
}
|
|
116
|
+
if (priceUsd != null) {
|
|
117
|
+
lines.push(`Price (USD): ${priceUsd}`);
|
|
118
|
+
}
|
|
119
|
+
if (mcap != null) {
|
|
120
|
+
lines.push(`Market cap (USD): ${mcap}`);
|
|
121
|
+
}
|
|
122
|
+
if (volume24h != null) {
|
|
123
|
+
lines.push(`24h volume (USD): ${volume24h}`);
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
return lines.join("\n");
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
async function handleTokenInfo(
|
|
130
|
+
request: AiRequest,
|
|
131
|
+
): Promise<AiResponse> {
|
|
132
|
+
const trimmed = request.input?.trim() ?? "";
|
|
133
|
+
const symbolCandidate = extractSymbolCandidate(trimmed);
|
|
134
|
+
|
|
135
|
+
if (!symbolCandidate) {
|
|
136
|
+
return {
|
|
137
|
+
ok: false,
|
|
138
|
+
provider: "openai",
|
|
139
|
+
mode: "token_info",
|
|
140
|
+
error: "Could not detect a token symbol. Try sending something like USDT.",
|
|
141
|
+
};
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
const tokenResult: TokenSearchResult = await getTokenBySymbol(
|
|
145
|
+
symbolCandidate,
|
|
146
|
+
);
|
|
147
|
+
|
|
148
|
+
if (!tokenResult.ok) {
|
|
149
|
+
return {
|
|
150
|
+
ok: false,
|
|
151
|
+
provider: "openai",
|
|
152
|
+
mode: "token_info",
|
|
153
|
+
error:
|
|
154
|
+
tokenResult.error === "not_found"
|
|
155
|
+
? `Token ${symbolCandidate} was not found on TON.`
|
|
156
|
+
: "Token service is temporarily unavailable.",
|
|
157
|
+
meta: {
|
|
158
|
+
symbol: symbolCandidate,
|
|
159
|
+
reason: tokenResult.reason,
|
|
160
|
+
status_code: tokenResult.status_code,
|
|
161
|
+
},
|
|
162
|
+
};
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
const token = tokenResult.data;
|
|
166
|
+
const facts = buildTokenFactsBlock(symbolCandidate, token);
|
|
167
|
+
|
|
168
|
+
const promptParts = [
|
|
169
|
+
"You are a concise TON token analyst.",
|
|
170
|
+
"",
|
|
171
|
+
"Facts about the token:",
|
|
172
|
+
facts,
|
|
173
|
+
"",
|
|
174
|
+
"User question or context:",
|
|
175
|
+
trimmed,
|
|
176
|
+
];
|
|
177
|
+
|
|
178
|
+
const composedInput = promptParts.join("\n");
|
|
179
|
+
|
|
180
|
+
const result = await callOpenAiChat("token_info", {
|
|
181
|
+
input: composedInput,
|
|
182
|
+
userId: request.userId,
|
|
183
|
+
context: {
|
|
184
|
+
...request.context,
|
|
185
|
+
symbol: symbolCandidate,
|
|
186
|
+
token,
|
|
187
|
+
source: "swap.coffee",
|
|
188
|
+
},
|
|
189
|
+
instructions: request.instructions,
|
|
190
|
+
});
|
|
191
|
+
|
|
192
|
+
return {
|
|
193
|
+
...result,
|
|
194
|
+
mode: "token_info",
|
|
195
|
+
meta: {
|
|
196
|
+
...(result.meta ?? {}),
|
|
197
|
+
symbol: symbolCandidate,
|
|
198
|
+
token,
|
|
199
|
+
},
|
|
200
|
+
};
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
export async function transmit(request: AiRequest): Promise<AiResponse> {
|
|
204
|
+
const mode: AiMode = request.mode ?? "chat";
|
|
205
|
+
const thread = request.threadContext;
|
|
206
|
+
|
|
207
|
+
if (thread && !thread.skipClaim) {
|
|
208
|
+
const skipped = await claimUserMessage(thread, request.input);
|
|
209
|
+
if (skipped) return skipped;
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
if (mode === "token_info") {
|
|
213
|
+
const result = await handleTokenInfo(request);
|
|
214
|
+
if (result.ok && result.output_text && thread) {
|
|
215
|
+
await persistAssistantMessage(thread, result.output_text);
|
|
216
|
+
}
|
|
217
|
+
return result;
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
let input = request.input;
|
|
221
|
+
if (thread) {
|
|
222
|
+
const history = await getThreadHistory({
|
|
223
|
+
user_telegram: thread.user_telegram,
|
|
224
|
+
thread_id: thread.thread_id,
|
|
225
|
+
type: thread.type,
|
|
226
|
+
limit: HISTORY_LIMIT,
|
|
227
|
+
});
|
|
228
|
+
input = formatHistoryForInput(history) + "Current message:\nuser: " + request.input;
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
const result = await callOpenAiChat(mode, {
|
|
232
|
+
input,
|
|
233
|
+
userId: request.userId,
|
|
234
|
+
context: request.context,
|
|
235
|
+
instructions: request.instructions,
|
|
236
|
+
});
|
|
237
|
+
|
|
238
|
+
if (result.ok && result.output_text && thread) {
|
|
239
|
+
await persistAssistantMessage(thread, result.output_text);
|
|
240
|
+
}
|
|
241
|
+
return result;
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
/** Stream AI response; onDelta(accumulatedText) is called for each chunk. Only the final OpenAI call is streamed. */
|
|
245
|
+
export async function transmitStream(
|
|
246
|
+
request: AiRequest,
|
|
247
|
+
onDelta: (text: string) => void | Promise<void>,
|
|
248
|
+
opts?: { isCancelled?: () => boolean; getAbortSignal?: () => Promise<boolean> },
|
|
249
|
+
): Promise<AiResponse> {
|
|
250
|
+
const mode: AiMode = request.mode ?? "chat";
|
|
251
|
+
const thread = request.threadContext;
|
|
252
|
+
|
|
253
|
+
if (thread && !thread.skipClaim) {
|
|
254
|
+
const skipped = await claimUserMessage(thread, request.input);
|
|
255
|
+
if (skipped) return skipped;
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
if (mode === "token_info") {
|
|
259
|
+
const tokenResult = await (async () => {
|
|
260
|
+
const trimmed = request.input?.trim() ?? "";
|
|
261
|
+
const symbolCandidate = extractSymbolCandidate(trimmed);
|
|
262
|
+
if (!symbolCandidate) {
|
|
263
|
+
return null;
|
|
264
|
+
}
|
|
265
|
+
return getTokenBySymbol(symbolCandidate);
|
|
266
|
+
})();
|
|
267
|
+
|
|
268
|
+
if (!tokenResult) {
|
|
269
|
+
return {
|
|
270
|
+
ok: false,
|
|
271
|
+
provider: "openai",
|
|
272
|
+
mode: "token_info",
|
|
273
|
+
error: "Could not detect a token symbol. Try sending something like USDT.",
|
|
274
|
+
};
|
|
275
|
+
}
|
|
276
|
+
if (!tokenResult.ok) {
|
|
277
|
+
const symbolCandidate = extractSymbolCandidate(request.input?.trim() ?? "");
|
|
278
|
+
return {
|
|
279
|
+
ok: false,
|
|
280
|
+
provider: "openai",
|
|
281
|
+
mode: "token_info",
|
|
282
|
+
error:
|
|
283
|
+
tokenResult.error === "not_found"
|
|
284
|
+
? `Token ${symbolCandidate ?? ""} was not found on TON.`
|
|
285
|
+
: "Token service is temporarily unavailable.",
|
|
286
|
+
meta: {
|
|
287
|
+
symbol: tokenResult.symbol,
|
|
288
|
+
reason: tokenResult.reason,
|
|
289
|
+
status_code: tokenResult.status_code,
|
|
290
|
+
},
|
|
291
|
+
};
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
const token = tokenResult.data;
|
|
295
|
+
const symbolCandidate = extractSymbolCandidate(request.input?.trim() ?? "")!;
|
|
296
|
+
const facts = buildTokenFactsBlock(symbolCandidate, token);
|
|
297
|
+
const trimmed = request.input?.trim() ?? "";
|
|
298
|
+
const promptParts = [
|
|
299
|
+
"You are a concise TON token analyst.",
|
|
300
|
+
"",
|
|
301
|
+
"Facts about the token:",
|
|
302
|
+
facts,
|
|
303
|
+
"",
|
|
304
|
+
"User question or context:",
|
|
305
|
+
trimmed,
|
|
306
|
+
];
|
|
307
|
+
const composedInput = promptParts.join("\n");
|
|
308
|
+
|
|
309
|
+
const result = await callOpenAiChatStream(
|
|
310
|
+
"token_info",
|
|
311
|
+
{
|
|
312
|
+
input: composedInput,
|
|
313
|
+
userId: request.userId,
|
|
314
|
+
context: {
|
|
315
|
+
...request.context,
|
|
316
|
+
symbol: symbolCandidate,
|
|
317
|
+
token,
|
|
318
|
+
source: "swap.coffee",
|
|
319
|
+
},
|
|
320
|
+
instructions: request.instructions,
|
|
321
|
+
},
|
|
322
|
+
onDelta,
|
|
323
|
+
opts,
|
|
324
|
+
);
|
|
325
|
+
|
|
326
|
+
if (result.ok && result.output_text && thread) {
|
|
327
|
+
await persistAssistantMessage(thread, result.output_text);
|
|
328
|
+
}
|
|
329
|
+
return {
|
|
330
|
+
...result,
|
|
331
|
+
mode: "token_info",
|
|
332
|
+
meta: {
|
|
333
|
+
...(result.meta ?? {}),
|
|
334
|
+
symbol: symbolCandidate,
|
|
335
|
+
token,
|
|
336
|
+
},
|
|
337
|
+
};
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
let input = request.input;
|
|
341
|
+
if (thread) {
|
|
342
|
+
const history = await getThreadHistory({
|
|
343
|
+
user_telegram: thread.user_telegram,
|
|
344
|
+
thread_id: thread.thread_id,
|
|
345
|
+
type: thread.type,
|
|
346
|
+
limit: HISTORY_LIMIT,
|
|
347
|
+
});
|
|
348
|
+
input = formatHistoryForInput(history) + "Current message:\nuser: " + request.input;
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
const result = await callOpenAiChatStream(
|
|
352
|
+
mode,
|
|
353
|
+
{
|
|
354
|
+
input,
|
|
355
|
+
userId: request.userId,
|
|
356
|
+
context: request.context,
|
|
357
|
+
instructions: request.instructions,
|
|
358
|
+
},
|
|
359
|
+
onDelta,
|
|
360
|
+
opts,
|
|
361
|
+
);
|
|
362
|
+
|
|
363
|
+
if (result.ok && result.output_text && thread) {
|
|
364
|
+
await persistAssistantMessage(thread, result.output_text);
|
|
365
|
+
}
|
|
366
|
+
return result;
|
|
367
|
+
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
header indent bag in fullsize<br>
|
|
2
|
+
rotation on swap page<br>
|
|
3
|
+
edit interval healthcheck<br>
|
|
4
|
+
Ticker link<br>
|
|
5
|
+
Scrolling in AI widget fix<br>
|
|
6
|
+
Finish bot parts isolation<br>
|
|
7
|
+
ld timeout<br>
|
|
8
|
+
AI bd<br>
|
|
9
|
+
Hovers<br>
|
|
10
|
+
Languages<br>
|
|
11
|
+
Favicon change<br>
|
|
12
|
+
Wallet creation and nickname generation<br>
|
|
13
|
+
Jetton lockup<br>
|
|
14
|
+
Theme in Chrome<br>
|
|
15
|
+
Bd<br>
|
|
16
|
+
Wallet creation<br>
|
|
17
|
+
Apps design<br>
|
|
18
|
+
Plan users bd for search<br>
|
|
19
|
+
Brand book<br>
|
|
20
|
+
Fix<br>
|
|
21
|
+
Starting Rewards<br>
|
|
22
|
+
Feed items types<br>
|
|
23
|
+
PAY BY QR<br>
|
|
24
|
+
swap long rate state<br>
|
|
25
|
+
swaps<br>
|
|
26
|
+
tokns<br>
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
Polyfils refactoring<br>
|
|
2
|
+
Wallet: Telegram, Connected, Unhosted<br>
|
|
3
|
+
Username loaded without intermediate state<br>
|
|
4
|
+
Bottom bar refactor<br>
|
|
5
|
+
AI options page<br>
|
|
6
|
+
Bottom bar icon review<br>
|
|
7
|
+
AI chat page<br>
|
|
8
|
+
Bring repository root app to demo design state<br>
|
|
9
|
+
GitHub and GitLab bidirectional mirroring<br>
|
|
10
|
+
readme.md<br>
|
|
11
|
+
Windows setup refactor<br>
|
|
12
|
+
https://azure.microsoft.com/en-us/products/artifact-signing<br>
|
|
13
|
+
README.md: Project news, discussions, where to learn for contributions<br>
|
|
14
|
+
|
|
15
|
+
Issues:<br>
|
|
16
|
+
Several chats async streaming<br>
|
|
17
|
+
|
|
18
|
+
Pull requests:<br>
|
|
19
|
+
AI responce checkup in bot (message interruption on new prompt)<br>
|
|
20
|
+
Typing in the middle of the text on large inputting<br>
|
|
21
|
+
|
|
22
|
+
To think about:<br>
|
|
23
|
+
Branding and naming
|
|
24
|
+
Place of the install update dialog in the UI<br>
|
|
25
|
+
Updater: fasten instant reload<br>
|
|
26
|
+
Custom installer design<br>
|
|
27
|
+
Fluent installer progress bar<br>
|
|
28
|
+
Optional cleanup of legacy Forge v5 deps<br>
|
|
29
|
+
Workflows dry mode (build check without releasing)<br>
|
|
30
|
+
Move Telegram folder contents to API<br>
|
|
31
|
+
https://www.npmjs.com/package/electron-log<br>
|
|
32
|
+
PGP release signing<br>
|
|
33
|
+
File's formats and folders structure<br>
|
|
34
|
+
Selection and pointing text detalization<br>
|
|
35
|
+
on word tapping places at the end of the word<br>
|
|
36
|
+
zoom-in cursor placing on long tap<br>
|
|
37
|
+
Instructions.ts in AI<br>
|
|
38
|
+
|
|
39
|
+
Abandoned:<br>
|
|
40
|
+
Placeholder refactor<br>
|
|
41
|
+
Logo bar visibility<br>
|
|
42
|
+
Overlay on screen expansion from mobile<br>
|
package/npmReadMe.md
CHANGED
|
@@ -33,12 +33,17 @@ registry and token.
|
|
|
33
33
|
|
|
34
34
|
## After Scaffold
|
|
35
35
|
|
|
36
|
+
Copy `npmrc.example` to `.npmrc` so installs match this repo (`legacy-peer-deps`; npm does not ship a real `.npmrc` in the tarball for security):
|
|
37
|
+
|
|
36
38
|
```bash
|
|
37
39
|
cd new-program
|
|
40
|
+
cp npmrc.example .npmrc
|
|
38
41
|
npm install
|
|
39
42
|
npm run start
|
|
40
43
|
```
|
|
41
44
|
|
|
45
|
+
If you prefer not to use a `.npmrc`, you can run **`npm install --legacy-peer-deps`** instead of the copy step.
|
|
46
|
+
|
|
42
47
|
Then open the project **`fullREADME.md`** for details (env vars, bot setup, build
|
|
43
48
|
and release commands).
|
|
44
49
|
|
|
@@ -49,5 +54,15 @@ and release commands).
|
|
|
49
54
|
|
|
50
55
|
## Notes
|
|
51
56
|
|
|
52
|
-
- Published
|
|
53
|
-
-
|
|
57
|
+
- Published from the repository root; the pack includes everything except patterns in [`.npmignore`](./.npmignore) (no `files` whitelist in `package.json`).
|
|
58
|
+
- `.npmrc` cannot be published on npm; `npmrc.example` is included so you can copy it locally.
|
|
59
|
+
|
|
60
|
+
## Matching local development
|
|
61
|
+
|
|
62
|
+
Use the same setup you would after cloning this repo:
|
|
63
|
+
|
|
64
|
+
1. **Node** — Prefer the version in [`.nvmrc`](./.nvmrc) (aligned with [`package.json`](./package.json) `engines`).
|
|
65
|
+
2. **npm install** — Copy [`npmrc.example`](./npmrc.example) to `.npmrc`, then run `npm install` (same `legacy-peer-deps` behavior as a local checkout with a root `.npmrc`).
|
|
66
|
+
3. **Env** — Copy [`.env.example`](./.env.example) to `.env` and fill variables (details in **`fullREADME.md`** after the CI readme swap, or in the main repo README).
|
|
67
|
+
|
|
68
|
+
The tarball does not ship `package-lock.json` (by [`.npmignore`](./.npmignore)); the first install generates a lockfile for your machine, like cloning without a committed lock.
|
package/npmrc.example
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
legacy-peer-deps=true
|
package/package.json
CHANGED
|
@@ -2,46 +2,21 @@
|
|
|
2
2
|
"name": "@www.hyperlinks.space/program-kit",
|
|
3
3
|
"productName": "Hyperlinks Space Program",
|
|
4
4
|
"description": "Hyperlinks Space Program",
|
|
5
|
-
"author": "
|
|
5
|
+
"author": "https://www.hyperlinks.space",
|
|
6
6
|
"license": "0BSD",
|
|
7
7
|
"private": false,
|
|
8
8
|
"bin": {
|
|
9
9
|
"program-kit": "scripts/program-kit-init.cjs"
|
|
10
10
|
},
|
|
11
|
-
"files": [
|
|
12
|
-
".env.example",
|
|
13
|
-
"README.md",
|
|
14
|
-
"fullREADME.md",
|
|
15
|
-
"npmReadMe.md",
|
|
16
|
-
"app",
|
|
17
|
-
"api",
|
|
18
|
-
"app.json",
|
|
19
|
-
"assets",
|
|
20
|
-
"blockchain",
|
|
21
|
-
"bot",
|
|
22
|
-
"database",
|
|
23
|
-
"docs",
|
|
24
|
-
"!docs/debug.log",
|
|
25
|
-
"!docs/issue.md",
|
|
26
|
-
"eas.json",
|
|
27
|
-
"eslint.config.js",
|
|
28
|
-
"global.css",
|
|
29
|
-
"package-lock.json",
|
|
30
|
-
"scripts",
|
|
31
|
-
"telegram",
|
|
32
|
-
"tsconfig.json",
|
|
33
|
-
"vercel.json",
|
|
34
|
-
"windows"
|
|
35
|
-
],
|
|
36
11
|
"publishConfig": {
|
|
37
12
|
"access": "public"
|
|
38
13
|
},
|
|
39
14
|
"repository": {
|
|
40
15
|
"type": "git",
|
|
41
|
-
"url": "https://github.com/HyperlinksSpace/
|
|
16
|
+
"url": "https://github.com/HyperlinksSpace/HyperlinksSpaceProgram.git"
|
|
42
17
|
},
|
|
43
18
|
"main": "index.js",
|
|
44
|
-
"version": "
|
|
19
|
+
"version": "123.123.123",
|
|
45
20
|
"type": "module",
|
|
46
21
|
"engines": {
|
|
47
22
|
"node": ">=18 <=22"
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Load before any module that touches Node's `Buffer` at evaluation time (e.g. @ton/*).
|
|
3
|
+
* Browsers and Telegram WebView do not define `globalThis.Buffer`.
|
|
4
|
+
*/
|
|
5
|
+
import { Buffer } from "buffer";
|
|
6
|
+
|
|
7
|
+
if (typeof globalThis !== "undefined" && !(globalThis as { Buffer?: unknown }).Buffer) {
|
|
8
|
+
(globalThis as { Buffer: typeof Buffer }).Buffer = Buffer;
|
|
9
|
+
}
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
## AI & Search bar input behaviour
|
|
2
|
+
|
|
3
|
+
This document describes how the text in the global AI & Search bottom bar should behave as the user types, matching the Flutter implementation.
|
|
4
|
+
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
### 1. Reference states (pictures sequence)
|
|
8
|
+
|
|
9
|
+
The reference images show a sequence of states for a long line of text; they illustrate how the bar grows and then turns into a scrolling window:
|
|
10
|
+
|
|
11
|
+
1. **Full text, no bar**
|
|
12
|
+
Long multi‑line text fills a tall content area. This is effectively the raw content without the constraints of the bottom bar.
|
|
13
|
+
|
|
14
|
+
2. **Initial bar: single line + arrow**
|
|
15
|
+
- Only a single line of text is visible.
|
|
16
|
+
- The text baseline is horizontally aligned with the apply arrow icon on the right.
|
|
17
|
+
- There is empty space above the line; bar height is at its minimum.
|
|
18
|
+
|
|
19
|
+
3. **Unconstrained multi‑line text**
|
|
20
|
+
- Text has grown to multiple lines in a taller, unbounded view (again, this is the raw content).
|
|
21
|
+
|
|
22
|
+
4. **Growing bar: multiple lines + arrow**
|
|
23
|
+
- The bottom bar has increased in height to show multiple lines.
|
|
24
|
+
- As lines are added, the **space above the text shrinks**, but the **last visible line remains on the same vertical level as the arrow**.
|
|
25
|
+
- Visually, the bar grows upwards while the arrow + last line baseline stays fixed.
|
|
26
|
+
|
|
27
|
+
5. **Very long text, no bar**
|
|
28
|
+
- The entire long text block is visible in a tall area, showing how much total content exists.
|
|
29
|
+
|
|
30
|
+
6. **Capped bar height: scrolling window**
|
|
31
|
+
- The bottom bar height is now capped (e.g. at 180 px).
|
|
32
|
+
- The visible area becomes a **fixed‑height window** into the text:
|
|
33
|
+
- Older lines at the top continue moving up and eventually disappear under the **top edge** of the bar as more text is entered.
|
|
34
|
+
- The **last visible line stays aligned with the arrow baseline** at the bottom of the bar. The typing position does not move vertically once the bar has reached its maximum height.
|
|
35
|
+
|
|
36
|
+
---
|
|
37
|
+
|
|
38
|
+
### 2. Detailed behaviour by line count
|
|
39
|
+
|
|
40
|
+
#### 1–7 lines: growing bar
|
|
41
|
+
|
|
42
|
+
- For each new line from 1 up to 7:
|
|
43
|
+
- The **bottom bar height increases** by exactly one line height (20 px).
|
|
44
|
+
- The height formula is:
|
|
45
|
+
\[
|
|
46
|
+
\text{height} = 20\text{ (top padding)} + N \times 20\text{ (lines)} + 20\text{ (bottom padding)}, \quad 1 \le N \le 7.
|
|
47
|
+
\]
|
|
48
|
+
- The **last line is always on the same baseline as the arrow** on the right.
|
|
49
|
+
- Visually, the bar grows **upwards**; the arrow + last line stay fixed at the bottom.
|
|
50
|
+
|
|
51
|
+
#### 8 lines: text reaches the top edge
|
|
52
|
+
|
|
53
|
+
- When the **8th line** appears:
|
|
54
|
+
- The text block now reaches the **top edge of the bottom bar**.
|
|
55
|
+
- The bar height is at its **maximum** (e.g. 180 px).
|
|
56
|
+
- All 8 lines are still visible at once, from the top edge down to the arrow.
|
|
57
|
+
|
|
58
|
+
#### 9 lines: full‑height text area, one line hidden
|
|
59
|
+
|
|
60
|
+
- When the **9th line** appears:
|
|
61
|
+
- The **scrollable text area is exactly 180 px high**, the same as the bar.
|
|
62
|
+
- The **last line remains aligned with the arrow** at the bottom.
|
|
63
|
+
- The **topmost line (1st)** is now hidden just above the top edge of the bar.
|
|
64
|
+
- If the user scrolls, they can reveal all 9 lines, because:
|
|
65
|
+
\[
|
|
66
|
+
9 \times 20\text{ px} = 180\text{ px},
|
|
67
|
+
\]
|
|
68
|
+
so all 9 lines can fit into the bar’s full height when scrolled to the appropriate position.
|
|
69
|
+
|
|
70
|
+
#### 9+ lines: fixed bar, 9‑line scrolling window
|
|
71
|
+
|
|
72
|
+
- For **any number of lines ≥ 9**:
|
|
73
|
+
- The bar height stays fixed at its maximum (e.g. 180 px).
|
|
74
|
+
- The **scrollable area always occupies the full bar height** (180 px).
|
|
75
|
+
- At any moment:
|
|
76
|
+
- Up to **9 lines are visible** in the window.
|
|
77
|
+
- The **bottom (last visible) line stays aligned with the arrow** while typing.
|
|
78
|
+
- Older lines scroll upwards and are hidden above the top edge; the user can scroll to reveal them.
|
|
79
|
+
|
|
80
|
+
---
|
|
81
|
+
|
|
82
|
+
### 3. Implementation‑oriented summary
|
|
83
|
+
|
|
84
|
+
- **Line height & padding**
|
|
85
|
+
- Line height: 20 px.
|
|
86
|
+
- Top padding: 20 px.
|
|
87
|
+
- Bottom padding: 20 px.
|
|
88
|
+
|
|
89
|
+
- **Bar growth vs. scroll mode**
|
|
90
|
+
- For 1–7 lines, bar height grows; arrow + last line baseline are fixed.
|
|
91
|
+
- From the 8th line onward, the bar stays at max height; the input switches to a scrollable window that:
|
|
92
|
+
- Always keeps the caret / last line baseline aligned with the arrow.
|
|
93
|
+
- Hides older lines under the top edge while allowing them to be revealed by scrolling.
|
|
94
|
+
|