Skip to content

Commit cc0e6b4

Browse files
committed
Support local media
1 parent bbb1fb0 commit cc0e6b4

File tree

4 files changed

+147
-6
lines changed

4 files changed

+147
-6
lines changed

codefox-common/src/common-path.ts

+8
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,14 @@ export const getModelStatusPath = (): string => {
4444
return modelStatusPath;
4545
};
4646

47+
//Media Directory
48+
export const getMediaDir = (): string =>
49+
ensureDir(path.join(getRootDir(), 'media'));
50+
export const getMediaPath = (modelName: string): string =>
51+
path.join(getModelsDir(), modelName);
52+
export const getMediaAvatarsDir = (): string =>
53+
ensureDir(path.join(getMediaDir(), 'avatars'));
54+
4755
// Models Directory
4856
export const getModelsDir = (): string =>
4957
ensureDir(path.join(getRootDir(), 'models'));
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,70 @@
1+
// For App Router: app/api/media/[...path]/route.ts
2+
import { NextRequest } from 'next/server';
3+
import fs from 'fs';
4+
import path from 'path';
5+
import { getMediaDir } from 'codefox-common';
6+
7+
export async function GET(
8+
request: NextRequest,
9+
{ params }: { params: { path: string[] } }
10+
) {
11+
try {
12+
// Get the media directory path
13+
const mediaDir = getMediaDir();
14+
15+
// Construct the full path to the requested file
16+
const filePath = path.join(mediaDir, ...params.path);
17+
18+
// Check if the file exists
19+
if (!fs.existsSync(filePath)) {
20+
// Log directory contents for debugging
21+
try {
22+
if (fs.existsSync(mediaDir)) {
23+
const avatarsDir = path.join(mediaDir, 'avatars');
24+
if (fs.existsSync(avatarsDir)) {
25+
console.log(
26+
'Avatars directory contents:',
27+
fs.readdirSync(avatarsDir)
28+
);
29+
} else {
30+
console.log('Avatars directory does not exist');
31+
}
32+
} else {
33+
console.log('Media directory does not exist');
34+
}
35+
} catch (err) {
36+
console.error('Error reading directory:', err);
37+
}
38+
39+
return new Response('File not found', { status: 404 });
40+
}
41+
42+
// Read the file
43+
const fileBuffer = fs.readFileSync(filePath);
44+
45+
// Determine content type based on file extension
46+
const ext = path.extname(filePath).toLowerCase();
47+
const contentTypeMap: Record<string, string> = {
48+
'.jpg': 'image/jpeg',
49+
'.jpeg': 'image/jpeg',
50+
'.png': 'image/png',
51+
'.webp': 'image/webp',
52+
'.gif': 'image/gif',
53+
};
54+
55+
const contentType = contentTypeMap[ext] || 'application/octet-stream';
56+
57+
// Return the file with appropriate headers
58+
return new Response(fileBuffer, {
59+
headers: {
60+
'Content-Type': contentType,
61+
'Cache-Control': 'public, max-age=31536000',
62+
},
63+
});
64+
} catch (error) {
65+
console.error('Error serving media file:', error);
66+
return new Response(`Error serving file: ${error.message}`, {
67+
status: 500,
68+
});
69+
}
70+
}

frontend/src/components/avatar-uploader.tsx

+30-3
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,31 @@ import { Avatar, AvatarFallback, AvatarImage } from '@/components/ui/avatar';
88
import { toast } from 'sonner';
99
import { useAuthContext } from '@/providers/AuthProvider';
1010

11+
// Avatar URL normalization helper
12+
function normalizeAvatarUrl(avatarUrl: string | null | undefined): string {
13+
if (!avatarUrl) return '';
14+
15+
// Check if it's already an absolute URL (S3 case)
16+
if (avatarUrl.startsWith('http://') || avatarUrl.startsWith('https://')) {
17+
return avatarUrl;
18+
}
19+
20+
// Check if it's a relative media path
21+
if (avatarUrl.startsWith('media/')) {
22+
// Convert to API route path
23+
return `/api/${avatarUrl}`;
24+
}
25+
26+
// Handle paths that might not have the media/ prefix
27+
if (avatarUrl.includes('avatars/')) {
28+
const parts = avatarUrl.split('avatars/');
29+
return `/api/media/avatars/${parts[parts.length - 1]}`;
30+
}
31+
32+
// Return as is for other cases
33+
return avatarUrl;
34+
}
35+
1136
interface AvatarUploaderProps {
1237
currentAvatarUrl: string;
1338
avatarFallback: string;
@@ -60,7 +85,9 @@ export const AvatarUploader: React.FC<AvatarUploaderProps> = ({
6085
});
6186

6287
if (data?.uploadAvatar?.success) {
63-
onAvatarChange(data.uploadAvatar.avatarUrl);
88+
// Store the original URL from backend
89+
const avatarUrl = data.uploadAvatar.avatarUrl;
90+
onAvatarChange(avatarUrl);
6491
toast.success('Avatar updated successfully');
6592

6693
// Refresh the user information in the auth context
@@ -84,8 +111,8 @@ export const AvatarUploader: React.FC<AvatarUploaderProps> = ({
84111
fileInputRef.current?.click();
85112
};
86113

87-
// Use preview URL if available, otherwise use the current avatar URL
88-
const displayUrl = previewUrl || currentAvatarUrl;
114+
// Use preview URL if available, otherwise use the normalized current avatar URL
115+
const displayUrl = previewUrl || normalizeAvatarUrl(currentAvatarUrl);
89116

90117
return (
91118
<div className="flex flex-col items-center gap-4">

frontend/src/components/user-settings.tsx

+39-3
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,31 @@ import { useMemo, useState, memo, useEffect } from 'react';
2020
import { EventEnum } from '../const/EventEnum';
2121
import { useAuthContext } from '@/providers/AuthProvider';
2222

23+
// Avatar URL normalization helper
24+
function normalizeAvatarUrl(avatarUrl: string | null | undefined): string {
25+
if (!avatarUrl) return '';
26+
27+
// Check if it's already an absolute URL (S3 case)
28+
if (avatarUrl.startsWith('http://') || avatarUrl.startsWith('https://')) {
29+
return avatarUrl;
30+
}
31+
32+
// Check if it's a relative media path
33+
if (avatarUrl.startsWith('media/')) {
34+
// Convert to API route path
35+
return `/api/${avatarUrl}`;
36+
}
37+
38+
// Handle paths that might not have the media/ prefix
39+
if (avatarUrl.includes('avatars/')) {
40+
const parts = avatarUrl.split('avatars/');
41+
return `/api/media/avatars/${parts[parts.length - 1]}`;
42+
}
43+
44+
// Return as is for other cases
45+
return avatarUrl;
46+
}
47+
2348
interface UserSettingsProps {
2449
isSimple: boolean;
2550
}
@@ -47,6 +72,11 @@ export const UserSettings = ({ isSimple }: UserSettingsProps) => {
4772
return user?.username || 'Anonymous';
4873
}, [isLoading, user?.username]);
4974

75+
// Normalize the avatar URL
76+
const normalizedAvatarUrl = useMemo(() => {
77+
return normalizeAvatarUrl(user?.avatarUrl);
78+
}, [user?.avatarUrl]);
79+
5080
const handleSettingsClick = () => {
5181
// First navigate using Next.js router
5282
router.push('/chat?id=setting');
@@ -68,9 +98,9 @@ export const UserSettings = ({ isSimple }: UserSettingsProps) => {
6898
}`}
6999
>
70100
<SmallAvatar className="flex items-center justify-center">
71-
{/* Use empty string fallback instead of undefined to avoid React warnings */}
101+
{/* Use normalized avatar URL */}
72102
<AvatarImage
73-
src={user?.avatarUrl || ''}
103+
src={normalizedAvatarUrl}
74104
alt="User"
75105
key={user?.avatarUrl}
76106
/>
@@ -79,7 +109,13 @@ export const UserSettings = ({ isSimple }: UserSettingsProps) => {
79109
{!isSimple && <span className="truncate">{displayUsername}</span>}
80110
</Button>
81111
);
82-
}, [avatarFallback, displayUsername, isSimple, user?.avatarUrl]);
112+
}, [
113+
avatarFallback,
114+
displayUsername,
115+
isSimple,
116+
normalizedAvatarUrl,
117+
user?.avatarUrl,
118+
]);
83119

84120
return (
85121
<DropdownMenu>

0 commit comments

Comments
 (0)