Skip to content

Commit 85726ae

Browse files
committed
v2.50 - Fix Rendering, @ Function
1 parent 6f0e799 commit 85726ae

10 files changed

Lines changed: 1006 additions & 497 deletions

File tree

frontend/src/components/app/ChatInput.tsx

Lines changed: 82 additions & 79 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ import { FILE_INPUT_ACCEPT, isSupportedFile, isWithinSizeLimit } from "@/lib/fil
1515
import { attachedToMessageFormat } from "@/lib/attachFiles"
1616
import { usePreparedAttachments } from "@/hooks/usePreparedAttachments"
1717
import { useAuth } from "@/context/AuthContext"
18-
import ContentTypePicker, { ContentTypeChips } from "@/components/ui/ContentTypePicker"
18+
import ContentTypePicker, { InlineContentChips, type ContentTypePickerKeyHandler } from "@/components/ui/ContentTypePicker"
1919
import { readStoredModel, writeStoredModel } from "@/lib/models"
2020
import { readStoredContentTypes, writeStoredContentTypes, buildContentTypeAutoPrompt } from "@/lib/contentTypes"
2121

@@ -73,6 +73,7 @@ const ChatInput = memo(function ChatInput() {
7373
const [micHover, setMicHover] = useState(false)
7474
const [sendHover, setSendHover] = useState(false)
7575
const fileInputRef = useRef<HTMLInputElement>(null)
76+
const pickerKeyRef = useRef<ContentTypePickerKeyHandler | null>(null)
7677
const localSendInFlightRef = useRef(false)
7778
const {
7879
attachments,
@@ -277,95 +278,96 @@ const ChatInput = memo(function ChatInput() {
277278
</div>
278279
)}
279280

280-
{/* Content type chips */}
281-
<ContentTypeChips
282-
selected={inputFeatures.requestedContentTypes ?? []}
283-
onRemove={(id) => {
284-
const next = (inputFeatures.requestedContentTypes ?? []).filter((t) => t !== id)
285-
setInputFeatures({ requestedContentTypes: next })
286-
writeStoredContentTypes(next)
287-
}}
288-
isDark={isDark}
289-
/>
290-
291-
{/* Row 1: Input area + typewriter placeholder — fixed width, text wraps within box */}
292-
<div style={{ position: "relative", height: `${inputHeight}px`, minHeight: `${ROW_HEIGHT}px`, width: "100%", minWidth: 0, overflow: "hidden" }}>
293-
<textarea
294-
ref={textareaRef}
295-
value={inputValue}
296-
onChange={handleChange}
297-
onPaste={handlePaste}
298-
onKeyDown={(e) => {
299-
if (e.key === "Enter" && !e.shiftKey) {
300-
e.preventDefault()
301-
void handleSend()
302-
}
303-
}}
304-
onFocus={() => setInputFocused(true)}
305-
onBlur={() => setInputFocused(false)}
306-
rows={1}
307-
aria-label="Message input"
308-
style={{
309-
width: "100%",
310-
maxWidth: "100%",
311-
boxSizing: "border-box",
312-
background: "transparent",
313-
border: "none",
314-
outline: "none",
315-
resize: "none",
316-
fontFamily: FONTS.sans,
317-
fontSize: "16px",
318-
fontWeight: 400,
319-
color: COLORS.textPrimary,
320-
lineHeight: `${ROW_HEIGHT}px`,
321-
padding: 0,
322-
margin: 0,
323-
height: `${inputHeight}px`,
324-
maxHeight: `${ROW_HEIGHT * 2}px`,
325-
overflowX: "hidden",
326-
overflowY: "hidden",
327-
overflowWrap: "break-word",
328-
wordBreak: "break-word",
329-
whiteSpace: "pre-wrap",
330-
position: "relative",
331-
zIndex: 2,
281+
{/* Row 1: inline content chips + input area + typewriter placeholder */}
282+
<div style={{ display: "flex", flexWrap: "wrap", alignItems: "center", gap: "6px", minHeight: `${ROW_HEIGHT}px`, width: "100%", minWidth: 0 }}>
283+
<InlineContentChips
284+
selected={inputFeatures.requestedContentTypes ?? []}
285+
onRemove={(id) => {
286+
const next = (inputFeatures.requestedContentTypes ?? []).filter((t) => t !== id)
287+
setInputFeatures({ requestedContentTypes: next })
288+
writeStoredContentTypes(next)
332289
}}
290+
isDark={isDark}
333291
/>
334-
335-
{/* Typewriter placeholder */}
336-
{!inputValue && !inputFocused && (
337-
<span
338-
aria-hidden="true"
292+
<div style={{ position: "relative", flex: 1, minWidth: "100px", height: `${inputHeight}px`, minHeight: `${ROW_HEIGHT}px`, overflow: "hidden" }}>
293+
<textarea
294+
ref={textareaRef}
295+
value={inputValue}
296+
onChange={handleChange}
297+
onPaste={handlePaste}
298+
onKeyDown={(e) => {
299+
if (atMention && pickerKeyRef.current?.handleKeyDown(e.key)) { e.preventDefault(); return }
300+
if (e.key === "Enter" && !e.shiftKey) {
301+
e.preventDefault()
302+
void handleSend()
303+
}
304+
}}
305+
onFocus={() => setInputFocused(true)}
306+
onBlur={() => setInputFocused(false)}
307+
rows={1}
308+
aria-label="Message input"
339309
style={{
340-
position: "absolute",
341-
left: 0,
342-
top: 0,
310+
width: "100%",
311+
maxWidth: "100%",
312+
boxSizing: "border-box",
313+
background: "transparent",
314+
border: "none",
315+
outline: "none",
316+
resize: "none",
343317
fontFamily: FONTS.sans,
344318
fontSize: "16px",
345319
fontWeight: 400,
346-
color: COLORS.textDimmed,
320+
color: COLORS.textPrimary,
347321
lineHeight: `${ROW_HEIGHT}px`,
348-
height: `${ROW_HEIGHT}px`,
349-
pointerEvents: "none",
350-
zIndex: 1,
351-
whiteSpace: "nowrap",
352-
display: "flex",
353-
alignItems: "center",
322+
padding: 0,
323+
margin: 0,
324+
height: `${inputHeight}px`,
325+
maxHeight: `${ROW_HEIGHT * 2}px`,
326+
overflowX: "hidden",
327+
overflowY: "hidden",
328+
overflowWrap: "break-word",
329+
wordBreak: "break-word",
330+
whiteSpace: "pre-wrap",
331+
position: "relative",
332+
zIndex: 2,
354333
}}
355-
>
356-
{typedText}
334+
/>
335+
336+
{/* Typewriter placeholder */}
337+
{!inputValue && !inputFocused && (inputFeatures.requestedContentTypes ?? []).length === 0 && (
357338
<span
339+
aria-hidden="true"
358340
style={{
359-
display: "inline-block",
360-
width: "1.5px",
361-
height: "22px",
362-
background: COLORS.textDimmed,
363-
marginLeft: "1px",
364-
animation: "cursorBlink 0.8s step-end infinite",
341+
position: "absolute",
342+
left: 0,
343+
top: 0,
344+
fontFamily: FONTS.sans,
345+
fontSize: "16px",
346+
fontWeight: 400,
347+
color: COLORS.textDimmed,
348+
lineHeight: `${ROW_HEIGHT}px`,
349+
height: `${ROW_HEIGHT}px`,
350+
pointerEvents: "none",
351+
zIndex: 1,
352+
whiteSpace: "nowrap",
353+
display: "flex",
354+
alignItems: "center",
365355
}}
366-
/>
367-
</span>
368-
)}
356+
>
357+
{typedText}
358+
<span
359+
style={{
360+
display: "inline-block",
361+
width: "1.5px",
362+
height: "22px",
363+
background: COLORS.textDimmed,
364+
marginLeft: "1px",
365+
animation: "cursorBlink 0.8s step-end infinite",
366+
}}
367+
/>
368+
</span>
369+
)}
370+
</div>
369371
</div>
370372

371373
{/* Row 2: Action buttons — preventDefault keeps focus on textarea when tapping icons */}
@@ -401,6 +403,7 @@ const ChatInput = memo(function ChatInput() {
401403
COLORS={COLORS}
402404
triggerSearch={atMention?.search}
403405
onTriggerConsumed={handleTriggerConsumed}
406+
keyHandlerRef={pickerKeyRef}
404407
/>
405408
{/* Web search toggle */}
406409
<button

frontend/src/components/block-types/Image.tsx

Lines changed: 74 additions & 83 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ interface ImageBlockProps {
1414
onAnalyze?: () => void
1515
}
1616

17-
/** Image block — media + optional title/caption + popup/lightbox on click. */
17+
/** Image block — card style: image + title + "Open" link, with lightbox on click. */
1818
export const ImageBlock = React.memo(function ImageBlock({
1919
src,
2020
alt = "",
@@ -26,6 +26,8 @@ export const ImageBlock = React.memo(function ImageBlock({
2626
const [failed, setFailed] = useState(false)
2727
const [popupOpen, setPopupOpen] = useState(false)
2828
const margin = variant === "card" ? "8px 0" : "16px 0"
29+
const isDark = (COLORS.bg ?? "#000").startsWith("#0") || (COLORS.bg ?? "#000") === "rgb(0,0,0)" || (COLORS.bg ?? "#000").includes("0A0A0A")
30+
const label = caption || alt || ""
2931

3032
const closePopup = useCallback(() => setPopupOpen(false), [])
3133
useEffect(() => {
@@ -44,96 +46,85 @@ export const ImageBlock = React.memo(function ImageBlock({
4446

4547
if (failed) {
4648
return (
47-
<figure style={{ margin }}>
48-
<div
49-
style={{
50-
padding: "24px",
51-
background: "rgba(128,128,128,0.1)",
52-
borderRadius: "12px",
53-
color: COLORS.textTertiary,
54-
fontFamily: FONTS.sans,
55-
fontSize: "13px",
56-
}}
57-
>
49+
<div style={{
50+
margin,
51+
borderRadius: "12px",
52+
background: isDark ? "rgba(255,255,255,0.02)" : "rgba(0,0,0,0.02)",
53+
border: `1px solid ${COLORS.border}`,
54+
overflow: "hidden",
55+
}}>
56+
<div style={{
57+
display: "flex", alignItems: "center", gap: "8px",
58+
padding: "10px 12px",
59+
borderBottom: `1px solid ${COLORS.border}`,
60+
}}>
61+
<span style={{ fontFamily: FONTS.mono, fontSize: "10px", textTransform: "uppercase", letterSpacing: "0.05em", color: COLORS.textTertiary }}>
62+
IMAGE
63+
</span>
64+
{label && (
65+
<span style={{ fontFamily: FONTS.sans, fontSize: "12px", color: COLORS.textSecondary, overflow: "hidden", textOverflow: "ellipsis", whiteSpace: "nowrap", flex: 1, minWidth: 0 }}>
66+
{label}
67+
</span>
68+
)}
69+
</div>
70+
<div style={{ padding: "24px 12px", fontFamily: FONTS.sans, fontSize: "13px", color: COLORS.textTertiary }}>
5871
Image failed to load
5972
</div>
60-
{caption && (
61-
<figcaption
62-
style={{
63-
fontFamily: FONTS.sans,
64-
fontSize: "13px",
65-
color: COLORS.textTertiary,
66-
marginTop: "6px",
67-
fontStyle: "italic",
68-
}}
69-
>
70-
{caption}
71-
</figcaption>
72-
)}
73-
</figure>
73+
</div>
7474
)
7575
}
7676

7777
return (
7878
<>
79-
<figure style={{ margin }}>
80-
{onAnalyze && variant !== "card" ? (
81-
<div style={{ marginBottom: "8px", display: "flex", justifyContent: "flex-end" }}>
82-
<button
83-
type="button"
84-
onClick={onAnalyze}
85-
style={{
86-
fontFamily: FONTS.sans,
87-
fontSize: "12px",
88-
fontWeight: 500,
89-
padding: "6px 12px",
90-
borderRadius: "6px",
91-
border: `1px solid ${COLORS.border}`,
92-
background: "rgba(255,255,255,0.04)",
93-
color: COLORS.accent,
94-
cursor: "pointer",
95-
}}
96-
>
97-
Analyze image
98-
</button>
99-
</div>
100-
) : null}
101-
<button
102-
type="button"
103-
onClick={() => setPopupOpen(true)}
104-
style={{
105-
padding: 0,
106-
border: "none",
107-
background: "none",
108-
cursor: "pointer",
109-
display: "block",
110-
width: "100%",
111-
textAlign: "left",
112-
}}
113-
aria-label="View image full size"
114-
>
115-
<img
116-
src={src}
117-
alt={alt}
118-
onError={() => setFailed(true)}
119-
style={{ maxWidth: "100%", borderRadius: "12px", display: "block" }}
120-
loading="lazy"
121-
/>
122-
</button>
123-
{(caption || alt) && (
124-
<figcaption
125-
style={{
126-
fontFamily: FONTS.sans,
127-
fontSize: "13px",
128-
color: COLORS.textTertiary,
129-
marginTop: "6px",
130-
fontStyle: "italic",
131-
}}
79+
<div style={{
80+
margin,
81+
borderRadius: "12px",
82+
background: isDark ? "rgba(255,255,255,0.02)" : "rgba(0,0,0,0.02)",
83+
border: `1px solid ${COLORS.border}`,
84+
overflow: "hidden",
85+
}}>
86+
{/* Header: type label + title + Open link */}
87+
<div style={{
88+
display: "flex", alignItems: "center", gap: "8px",
89+
padding: "10px 12px",
90+
borderBottom: `1px solid ${COLORS.border}`,
91+
background: isDark ? "rgba(255,255,255,0.02)" : "rgba(0,0,0,0.02)",
92+
}}>
93+
<span style={{ fontFamily: FONTS.mono, fontSize: "10px", textTransform: "uppercase", letterSpacing: "0.05em", color: COLORS.textTertiary }}>
94+
IMAGE
95+
</span>
96+
{label && (
97+
<span style={{ fontFamily: FONTS.sans, fontSize: "12px", color: COLORS.textSecondary, overflow: "hidden", textOverflow: "ellipsis", whiteSpace: "nowrap", flex: 1, minWidth: 0 }}>
98+
{label}
99+
</span>
100+
)}
101+
<a
102+
href={src}
103+
target="_blank"
104+
rel="noreferrer"
105+
style={{ marginLeft: "auto", fontFamily: FONTS.sans, fontSize: "11px", color: COLORS.accent, textDecoration: "none", flexShrink: 0 }}
132106
>
133-
{caption || alt}
134-
</figcaption>
135-
)}
136-
</figure>
107+
Open
108+
</a>
109+
</div>
110+
{/* Image body */}
111+
<div style={{ padding: "10px 12px" }}>
112+
<button
113+
type="button"
114+
onClick={() => setPopupOpen(true)}
115+
style={{ padding: 0, border: "none", background: "none", cursor: "pointer", display: "block", width: "100%", textAlign: "left" }}
116+
aria-label="View image full size"
117+
>
118+
<img
119+
src={src}
120+
alt={alt}
121+
onError={() => setFailed(true)}
122+
style={{ width: "100%", maxHeight: "420px", objectFit: "contain", borderRadius: "8px", display: "block", background: isDark ? "rgba(0,0,0,0.2)" : "rgba(0,0,0,0.05)" }}
123+
loading="lazy"
124+
/>
125+
</button>
126+
</div>
127+
</div>
137128

138129
{popupOpen && (
139130
<div

0 commit comments

Comments
 (0)