Compare commits
6 Commits
fix/image-
...
extend-jwt
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
07c272c96a | ||
|
|
975a337af4 | ||
|
|
e644def141 | ||
|
|
be600e78d6 | ||
|
|
b6576fb2fd | ||
|
|
bb3ef4fe95 |
3
app.py
3
app.py
@@ -1,5 +1,6 @@
|
|||||||
import logging
|
import logging
|
||||||
import os
|
import os
|
||||||
|
from datetime import timedelta
|
||||||
|
|
||||||
from dotenv import load_dotenv
|
from dotenv import load_dotenv
|
||||||
from quart import Quart, jsonify, render_template, request, send_from_directory
|
from quart import Quart, jsonify, render_template, request, send_from_directory
|
||||||
@@ -38,6 +39,8 @@ app = Quart(
|
|||||||
)
|
)
|
||||||
|
|
||||||
app.config["JWT_SECRET_KEY"] = os.getenv("JWT_SECRET_KEY", "SECRET_KEY")
|
app.config["JWT_SECRET_KEY"] = os.getenv("JWT_SECRET_KEY", "SECRET_KEY")
|
||||||
|
app.config["JWT_ACCESS_TOKEN_EXPIRES"] = timedelta(hours=1)
|
||||||
|
app.config["JWT_REFRESH_TOKEN_EXPIRES"] = timedelta(days=30)
|
||||||
app.config["MAX_CONTENT_LENGTH"] = 10 * 1024 * 1024 # 10 MB upload limit
|
app.config["MAX_CONTENT_LENGTH"] = 10 * 1024 * 1024 # 10 MB upload limit
|
||||||
jwt = JWTManager(app)
|
jwt = JWTManager(app)
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { useEffect, useState, useRef } from "react";
|
import { useCallback, useEffect, useState, useRef } from "react";
|
||||||
import { LogOut, Shield, PanelLeftClose, PanelLeftOpen, Menu, X } from "lucide-react";
|
import { LogOut, Shield, PanelLeftClose, PanelLeftOpen, Menu, X } from "lucide-react";
|
||||||
import { conversationService } from "../api/conversationService";
|
import { conversationService } from "../api/conversationService";
|
||||||
import { userService } from "../api/userService";
|
import { userService } from "../api/userService";
|
||||||
@@ -63,9 +63,13 @@ export const ChatScreen = ({ setAuthenticated }: ChatScreenProps) => {
|
|||||||
const abortControllerRef = useRef<AbortController | null>(null);
|
const abortControllerRef = useRef<AbortController | null>(null);
|
||||||
const simbaAnswers = ["meow.", "hiss...", "purrrrrr", "yowOWROWWowowr"];
|
const simbaAnswers = ["meow.", "hiss...", "purrrrrr", "yowOWROWWowowr"];
|
||||||
|
|
||||||
const scrollToBottom = () => {
|
const scrollToBottom = useCallback(() => {
|
||||||
messagesEndRef.current?.scrollIntoView({ behavior: "smooth" });
|
requestAnimationFrame(() => {
|
||||||
};
|
messagesEndRef.current?.scrollIntoView({
|
||||||
|
behavior: isLoading ? "instant" : "smooth",
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}, [isLoading]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
isMountedRef.current = true;
|
isMountedRef.current = true;
|
||||||
@@ -130,7 +134,7 @@ export const ChatScreen = ({ setAuthenticated }: ChatScreenProps) => {
|
|||||||
load();
|
load();
|
||||||
}, [selectedConversation?.id]);
|
}, [selectedConversation?.id]);
|
||||||
|
|
||||||
const handleQuestionSubmit = async () => {
|
const handleQuestionSubmit = useCallback(async () => {
|
||||||
if ((!query.trim() && !pendingImage) || isLoading) return;
|
if ((!query.trim() && !pendingImage) || isLoading) return;
|
||||||
|
|
||||||
let activeConversation = selectedConversation;
|
let activeConversation = selectedConversation;
|
||||||
@@ -214,19 +218,22 @@ export const ChatScreen = ({ setAuthenticated }: ChatScreenProps) => {
|
|||||||
if (isMountedRef.current) setIsLoading(false);
|
if (isMountedRef.current) setIsLoading(false);
|
||||||
abortControllerRef.current = null;
|
abortControllerRef.current = null;
|
||||||
}
|
}
|
||||||
};
|
}, [query, pendingImage, isLoading, selectedConversation, simbaMode, messages, setAuthenticated]);
|
||||||
|
|
||||||
const handleQueryChange = (event: React.ChangeEvent<HTMLTextAreaElement>) => {
|
const handleQueryChange = useCallback((event: React.ChangeEvent<HTMLTextAreaElement>) => {
|
||||||
setQuery(event.target.value);
|
setQuery(event.target.value);
|
||||||
};
|
}, []);
|
||||||
|
|
||||||
const handleKeyDown = (event: React.ChangeEvent<HTMLTextAreaElement>) => {
|
const handleKeyDown = useCallback((event: React.ChangeEvent<HTMLTextAreaElement>) => {
|
||||||
const kev = event as unknown as React.KeyboardEvent<HTMLTextAreaElement>;
|
const kev = event as unknown as React.KeyboardEvent<HTMLTextAreaElement>;
|
||||||
if (kev.key === "Enter" && !kev.shiftKey) {
|
if (kev.key === "Enter" && !kev.shiftKey) {
|
||||||
kev.preventDefault();
|
kev.preventDefault();
|
||||||
handleQuestionSubmit();
|
handleQuestionSubmit();
|
||||||
}
|
}
|
||||||
};
|
}, [handleQuestionSubmit]);
|
||||||
|
|
||||||
|
const handleImageSelect = useCallback((file: File) => setPendingImage(file), []);
|
||||||
|
const handleClearImage = useCallback(() => setPendingImage(null), []);
|
||||||
|
|
||||||
const handleLogout = () => {
|
const handleLogout = () => {
|
||||||
localStorage.removeItem("access_token");
|
localStorage.removeItem("access_token");
|
||||||
@@ -380,8 +387,8 @@ export const ChatScreen = ({ setAuthenticated }: ChatScreenProps) => {
|
|||||||
setSimbaMode={setSimbaMode}
|
setSimbaMode={setSimbaMode}
|
||||||
isLoading={isLoading}
|
isLoading={isLoading}
|
||||||
pendingImage={pendingImage}
|
pendingImage={pendingImage}
|
||||||
onImageSelect={(file) => setPendingImage(file)}
|
onImageSelect={handleImageSelect}
|
||||||
onClearImage={() => setPendingImage(null)}
|
onClearImage={handleClearImage}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -416,7 +423,7 @@ export const ChatScreen = ({ setAuthenticated }: ChatScreenProps) => {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<footer className="border-t border-sand-light/40 bg-cream/80 backdrop-blur-sm">
|
<footer className="border-t border-sand-light/40 bg-cream">
|
||||||
<div className="max-w-2xl mx-auto px-4 py-3">
|
<div className="max-w-2xl mx-auto px-4 py-3">
|
||||||
<MessageInput
|
<MessageInput
|
||||||
query={query}
|
query={query}
|
||||||
@@ -425,6 +432,9 @@ export const ChatScreen = ({ setAuthenticated }: ChatScreenProps) => {
|
|||||||
handleQuestionSubmit={handleQuestionSubmit}
|
handleQuestionSubmit={handleQuestionSubmit}
|
||||||
setSimbaMode={setSimbaMode}
|
setSimbaMode={setSimbaMode}
|
||||||
isLoading={isLoading}
|
isLoading={isLoading}
|
||||||
|
pendingImage={pendingImage}
|
||||||
|
onImageSelect={(file) => setPendingImage(file)}
|
||||||
|
onClearImage={() => setPendingImage(null)}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</footer>
|
</footer>
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { useRef, useState } from "react";
|
import React, { useEffect, useMemo, useRef, useState } from "react";
|
||||||
import { ArrowUp, ImagePlus, X } from "lucide-react";
|
import { ArrowUp, ImagePlus, X } from "lucide-react";
|
||||||
import { cn } from "../lib/utils";
|
import { cn } from "../lib/utils";
|
||||||
import { Textarea } from "./ui/textarea";
|
import { Textarea } from "./ui/textarea";
|
||||||
@@ -15,7 +15,7 @@ type MessageInputProps = {
|
|||||||
onClearImage: () => void;
|
onClearImage: () => void;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const MessageInput = ({
|
export const MessageInput = React.memo(({
|
||||||
query,
|
query,
|
||||||
handleKeyDown,
|
handleKeyDown,
|
||||||
handleQueryChange,
|
handleQueryChange,
|
||||||
@@ -29,6 +29,18 @@ export const MessageInput = ({
|
|||||||
const [simbaMode, setLocalSimbaMode] = useState(false);
|
const [simbaMode, setLocalSimbaMode] = useState(false);
|
||||||
const fileInputRef = useRef<HTMLInputElement>(null);
|
const fileInputRef = useRef<HTMLInputElement>(null);
|
||||||
|
|
||||||
|
// Create blob URL once per file, revoke on cleanup
|
||||||
|
const previewUrl = useMemo(
|
||||||
|
() => (pendingImage ? URL.createObjectURL(pendingImage) : null),
|
||||||
|
[pendingImage],
|
||||||
|
);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
return () => {
|
||||||
|
if (previewUrl) URL.revokeObjectURL(previewUrl);
|
||||||
|
};
|
||||||
|
}, [previewUrl]);
|
||||||
|
|
||||||
const toggleSimbaMode = () => {
|
const toggleSimbaMode = () => {
|
||||||
const next = !simbaMode;
|
const next = !simbaMode;
|
||||||
setLocalSimbaMode(next);
|
setLocalSimbaMode(next);
|
||||||
@@ -59,7 +71,7 @@ export const MessageInput = ({
|
|||||||
<div className="px-3 pt-3">
|
<div className="px-3 pt-3">
|
||||||
<div className="relative inline-block">
|
<div className="relative inline-block">
|
||||||
<img
|
<img
|
||||||
src={URL.createObjectURL(pendingImage)}
|
src={previewUrl!}
|
||||||
alt="Pending upload"
|
alt="Pending upload"
|
||||||
className="h-20 rounded-lg object-cover border border-sand"
|
className="h-20 rounded-lg object-cover border border-sand"
|
||||||
/>
|
/>
|
||||||
@@ -145,4 +157,4 @@ export const MessageInput = ({
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
});
|
||||||
|
|||||||
Reference in New Issue
Block a user