Skip to content

Commit 8e5b443

Browse files
committed
fix: UI improvements
1 parent 27d1ae2 commit 8e5b443

10 files changed

Lines changed: 97 additions & 45 deletions

File tree

.github/workflows/docker-build-manual.yaml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,3 +25,4 @@ jobs:
2525
platforms: linux/arm64
2626
push: true
2727
tags: sunjay195/chatty-ai-backend:latest
28+
cache-to: type=gha,mode=max

.github/workflows/docker-build-push.yaml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -68,6 +68,7 @@ jobs:
6868
platforms: linux/arm64
6969
push: true
7070
tags: sunjay195/chatty-ai-backend:latest
71+
cache-to: type=gha,mode=max
7172
labels: |
7273
git_commit_sha=${{ github.sha }}
7374
git_commit_short=${{ steps.meta.outputs.short_sha }}

backend/src/controllers/message.controllers.ts

Lines changed: 39 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,38 @@ import { type Context } from "hono";
22
import { scheduleMemoryExtraction, generateAIResponse, extractImageData } from "../utils/model.utils.js";
33
import prisma from "../config/prisma.config.js";
44
import { NotFoundError } from "../utils/appError.utils.js";
5-
import { streamText } from "hono/streaming";
5+
import { streamText as honoStreamText } from "hono/streaming";
66
import logger from "../utils/logger.utils.js";
7+
import { streamLoading, streamText, streamError } from "../utils/stream.utils.js";
8+
import { type UploadedFile } from "@app/shared/src/schemas/message.schema.js"
9+
10+
async function handleImageExtraction(messageFiles: UploadedFile[], stream: any): Promise<string> {
11+
if (messageFiles?.length === 0) return "";
12+
13+
try {
14+
await streamLoading(stream, "Analyzing image...");
15+
const extractionPromises = messageFiles.map((file: UploadedFile) => {
16+
if (file.fileUrl) {
17+
return extractImageData(file.fileUrl);
18+
}
19+
return Promise.resolve(null);
20+
});
21+
22+
const extractionResults = await Promise.allSettled(extractionPromises);
23+
24+
const successfulData = extractionResults
25+
.filter((result) => result.status === "fulfilled" && result.value)
26+
.map((result: any) => result.value as string);
27+
28+
if (successfulData.length > 0) {
29+
return `\n\nExtracted information from attached images:\n${successfulData.join("\n---\n")}\n\nUse this information if relevant when answering.`;
30+
}
31+
} catch (error) {
32+
logger.error({ message: "Failed to process attached images", error });
33+
}
34+
35+
return "";
36+
}
737

838
export async function handleUserMessageResponse(c: Context) {
939
const requestStartTime = new Date();
@@ -12,33 +42,11 @@ export async function handleUserMessageResponse(c: Context) {
1242
const { query, model, messageFiles } = c.get("body");
1343
const timezone = c.req.header("x-client-timezone") || "UTC";
1444

15-
return streamText(c, async (stream) => {
45+
return honoStreamText(c, async (stream) => {
1646
let fullResponse = "";
1747
let finalQuery = query;
1848

19-
try {
20-
if (messageFiles && Array.isArray(messageFiles) && messageFiles.length > 0) {
21-
await stream.write(JSON.stringify({ type: "loading", content: "Analyzing image..." }) + "\n");
22-
const extractionPromises = messageFiles.map((file: any) => {
23-
if (file.fileUrl) {
24-
return extractImageData(file.fileUrl);
25-
}
26-
return Promise.resolve(null);
27-
});
28-
29-
const extractionResults = await Promise.allSettled(extractionPromises);
30-
31-
const successfulData = extractionResults
32-
.filter((result) => result.status === "fulfilled" && result.value)
33-
.map((result: any) => result.value as string);
34-
35-
if (successfulData.length > 0) {
36-
finalQuery += `\n\nExtracted information from attached images:\n${successfulData.join("\n---\n")}\n\nUse this information if relevant when answering.`;
37-
}
38-
}
39-
} catch (error) {
40-
logger.error({ message: "Failed to process attached images", error });
41-
}
49+
finalQuery += await handleImageExtraction(messageFiles, stream);
4250

4351
try {
4452
const [chat, preferences, memories] = await Promise.all([
@@ -48,7 +56,7 @@ export async function handleUserMessageResponse(c: Context) {
4856
]);
4957

5058
if (!chat) {
51-
await stream.write(JSON.stringify({ type: "error", content: "Chat not found or unauthorized" }) + "\n");
59+
await streamError(stream, "Chat not found or unauthorized");
5260
return;
5361
}
5462

@@ -64,15 +72,15 @@ export async function handleUserMessageResponse(c: Context) {
6472
let isFirstChunk = true;
6573
for await (const chunk of aiStream) {
6674
if (isFirstChunk) {
67-
await stream.write(JSON.stringify({ type: "loading", content: null }) + "\n");
75+
await streamLoading(stream, null);
6876
isFirstChunk = false;
6977
}
7078
fullResponse += chunk;
71-
await stream.write(JSON.stringify({ type: "text", content: chunk }) + "\n");
79+
await streamText(stream, chunk);
7280
}
7381

7482
if (isFirstChunk) {
75-
await stream.write(JSON.stringify({ type: "loading", content: null }) + "\n");
83+
await streamLoading(stream, null);
7684
}
7785

7886
const filesToCreate = messageFiles?.length ? messageFiles : undefined;
@@ -84,7 +92,7 @@ export async function handleUserMessageResponse(c: Context) {
8492
messages: {
8593
create: [
8694
{
87-
text: finalQuery,
95+
text: query,
8896
role: "USER",
8997
createdAt: requestStartTime,
9098
messageFiles: filesToCreate ? { create: filesToCreate } : undefined,
@@ -102,7 +110,7 @@ export async function handleUserMessageResponse(c: Context) {
102110
chatId,
103111
error: error.message,
104112
});
105-
await stream.write(JSON.stringify({ type: "error", content: "Response generation interrupted" }) + "\n");
113+
await streamError(stream, "Response generation interrupted");
106114
}
107115
});
108116
}

backend/src/utils/stream.utils.ts

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
2+
type Streamable = { write: (str: string) => Promise<any> };
3+
4+
type StreamEventType = "text" | "loading" | "error";
5+
6+
type StreamEvent =
7+
| { type: "text"; content: string }
8+
| { type: "loading"; content: string | null }
9+
| { type: "error"; content: string };
10+
11+
export function streamEvent(stream: Streamable, event: StreamEvent): Promise<void> {
12+
return stream.write(JSON.stringify(event) + "\n");
13+
}
14+
15+
export function streamText(stream: Streamable, content: string): Promise<void> {
16+
return streamEvent(stream, { type: "text", content });
17+
}
18+
19+
export function streamLoading(stream: Streamable, content: string | null): Promise<void> {
20+
return streamEvent(stream, { type: "loading", content });
21+
}
22+
23+
export function streamError(stream: Streamable, content: string): Promise<void> {
24+
return streamEvent(stream, { type: "error", content });
25+
}
26+
27+
export type { StreamEventType, StreamEvent };

frontend/index.html

Lines changed: 14 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,16 @@
11
<!doctype html>
22
<html lang="en">
3-
<head>
4-
<meta charset="UTF-8" />
5-
<link rel="icon" type="image/webp" href="/favicon.ico" />
6-
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
7-
<title>Sleek AI</title>
8-
</head>
9-
<body>
10-
<div id="root"></div>
11-
<script type="module" src="/src/main.tsx"></script>
12-
</body>
13-
</html>
3+
4+
<head>
5+
<meta charset="UTF-8" />
6+
<link rel="icon" type="image/webp" href="/favicon.ico" />
7+
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
8+
<title>Sleek AI</title>
9+
</head>
10+
11+
<body>
12+
<div id="root"></div>
13+
<script type="module" src="/src/main.tsx"></script>
14+
</body>
15+
16+
</html>

frontend/src/components/messages/ModelMessage.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -59,7 +59,7 @@ type Props = {
5959
export default memo(function ModelMessage({ text, status, isCopied, onCopy, onResend, hideToolTip, isGenerating }: Props) {
6060

6161
return (
62-
<div className="max-w-full text-sm py-4 rounded-xl">
62+
<div className="max-w-full text-base md:text-sm py-4 rounded-xl">
6363
{isGenerating && text.length === 0 && !status ? (
6464
<span className="inline-block animate-pulse font-bold"></span>
6565
) : (

frontend/src/components/messages/UserMessage.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -89,7 +89,7 @@ export default memo(function UserMessage({ message, isCopied, onCopy }: Props) {
8989
</button>
9090
)}
9191

92-
<p className="text-sm leading-normal whitespace-pre-wrap break-words">{displayedText}</p>
92+
<p className="text-base md:text-sm leading-normal whitespace-pre-wrap break-words">{displayedText}</p>
9393
</div>
9494
)}
9595

frontend/src/components/sidebars/Sidebar.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -80,7 +80,7 @@ export default function Sidebar({ chats, isFetchingChats, onDeleteRequest, onRen
8080

8181
return (
8282
<>
83-
{isMobile && !collapsed && <div className="fixed inset-0 bg-black/40 z-10" onClick={() => setCollapsed(true)} />}
83+
{isMobile && !collapsed && <div className="fixed inset-0 bg-black/40 z-20" onClick={() => setCollapsed(true)} />}
8484
<aside
8585
ref={sidebarRef}
8686
className="fixed md:relative h-dvh w-3/4 sm:w-64 rounded-r-3xl sm:rounded-r-none bg-dark border-r border-gray-500/20 flex flex-col z-30 shrink-0"

frontend/src/styles/index.css

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,7 @@ body {
6060
.text-secondary {
6161
color: #fbfbfb;
6262
}
63+
6364
/*
6465
.word-break {
6566
word-break: break-word;
@@ -249,7 +250,14 @@ img {
249250
* {
250251
@apply border-border outline-ring/50;
251252
}
253+
252254
body {
253255
@apply bg-background text-foreground;
254256
}
255257
}
258+
259+
@media (max-width: 640px) {
260+
textarea {
261+
height: 40px;
262+
}
263+
}

shared/src/schemas/message.schema.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,3 +13,7 @@ export const messageSchema = z.object({
1313

1414
messageFiles: z.array(uploadedFileSchema).optional().default([]),
1515
});
16+
17+
export type UploadedFile = z.infer<typeof uploadedFileSchema>;
18+
19+
export type Message = z.infer<typeof messageSchema>;

0 commit comments

Comments
 (0)