refactor: migrate IRC Webchat components to Lua package structure and update ChatTabContent

This commit is contained in:
2025-12-30 01:08:03 +00:00
parent a3d445565a
commit 0690ab79c3
11 changed files with 148 additions and 297 deletions

View File

@@ -1,19 +1,19 @@
import type { User } from '@/lib/level-types'
import { Box, Typography } from '@mui/material'
import { IRCWebchatDeclarative } from '../../misc/demos/IRCWebchatDeclarative'
import { ResultsPane } from '../sections/ResultsPane'
interface ChatTabContentProps {
user: User
}
export function ChatTabContent({ user }: ChatTabContentProps) {
export function ChatTabContent() {
return (
<ResultsPane
title="Webchat"
description="Collaborate with other builders in real-time via the IRC channel."
>
<IRCWebchatDeclarative user={user} channelName="general" />
<Box sx={{ p: 4, border: '2px dashed', borderColor: 'divider', borderRadius: 1 }}>
<Typography variant="h6">IRC Webchat</Typography>
<Typography variant="body2" color="text.secondary">
This component is now a Lua package. See packages/irc_webchat/
</Typography>
</Box>
</ResultsPane>
)
}

View File

@@ -94,7 +94,7 @@ export function Level2({ user, onLogout, onNavigate }: Level2Props) {
</TabsContent>
<TabsContent value="chat" className="space-y-6">
<ChatTabContent user={currentUser} />
<ChatTabContent />
</TabsContent>
</Tabs>
</div>

View File

@@ -1,13 +0,0 @@
/**
* This file has been refactored into modular lambda-per-file structure.
*
* Import individual functions or use the class wrapper:
* @example
* import { IRCWebchatDeclarative } from './IRCWebchatDeclarative'
*
* @example
* import { IRCWebchatDeclarativeUtils } from './IRCWebchatDeclarative'
* IRCWebchatDeclarativeUtils.IRCWebchatDeclarative(...)
*/
export * from './IRCWebchatDeclarative'

View File

@@ -1,172 +0,0 @@
import { Gear, PaperPlaneTilt, SignOut, Users } from '@phosphor-icons/react'
import {
Badge,
Button,
Card,
CardContent,
CardHeader,
CardTitle,
Input,
ScrollArea,
} from '@/components/ui'
import type { ChatMessage } from './types'
import { UserList } from './UserList'
interface ChatWindowProps {
channelName: string
messages: ChatMessage[]
formattedTimes: Record<string, string>
onlineUsers: string[]
inputMessage: string
onInputChange: (value: string) => void
onSendMessage: () => void
onToggleSettings: () => void
showSettings: boolean
onClose?: () => void
onInputKeyPress?: (event: React.KeyboardEvent) => void
}
export function ChatWindow({
channelName,
messages,
formattedTimes,
onlineUsers,
inputMessage,
onInputChange,
onSendMessage,
onToggleSettings,
showSettings,
onClose,
onInputKeyPress,
}: ChatWindowProps) {
const getMessageStyle = (message: ChatMessage) => {
if (
message.type === 'system' ||
message.type === 'join' ||
message.type === 'leave' ||
message.type === 'command'
) {
return 'text-muted-foreground italic text-sm'
}
return ''
}
return (
<Card className="h-[600px] flex flex-col">
<CardHeader className="border-b border-border pb-3">
<div className="flex items-center justify-between">
<CardTitle className="flex items-center gap-2 text-lg">
<span className="font-mono">#</span>
{channelName}
</CardTitle>
<div className="flex items-center gap-2">
<Badge variant="secondary" className="gap-1.5">
<Users size={14} />
{onlineUsers.length}
</Badge>
<Button size="sm" variant="ghost" onClick={onToggleSettings}>
<Gear size={16} />
</Button>
{onClose && (
<Button size="sm" variant="ghost" onClick={onClose}>
<SignOut size={16} />
</Button>
)}
</div>
</div>
</CardHeader>
<CardContent className="flex-1 flex flex-col p-0 overflow-hidden">
<div className="flex flex-1 overflow-hidden">
<ScrollArea className="flex-1 p-4">
<div className="space-y-2 font-mono text-sm">
{messages.map(message => (
<div key={message.id} className={getMessageStyle(message)}>
{message.type === 'message' && (
<div className="flex gap-2">
<span className="text-muted-foreground shrink-0">
{formattedTimes[message.id] || ''}
</span>
<span className="font-semibold shrink-0 text-primary">
&lt;{message.username}&gt;
</span>
<span className="break-words">{message.message}</span>
</div>
)}
{message.type === 'system' && message.username === 'System' && (
<div className="flex gap-2">
<span className="text-muted-foreground shrink-0">
{formattedTimes[message.id] || ''}
</span>
<span>*** {message.message}</span>
</div>
)}
{message.type === 'system' && message.username !== 'System' && (
<div className="flex gap-2">
<span className="text-muted-foreground shrink-0">
{formattedTimes[message.id] || ''}
</span>
<span className="text-accent">
* {message.username} {message.message}
</span>
</div>
)}
{(message.type === 'join' || message.type === 'leave') && (
<div className="flex gap-2">
<span className="text-muted-foreground shrink-0">
{formattedTimes[message.id] || ''}
</span>
<span
className={message.type === 'join' ? 'text-green-500' : 'text-orange-500'}
>
--&gt; {message.message}
</span>
</div>
)}
{message.type === 'command' && (
<div className="flex gap-2">
<span className="text-muted-foreground shrink-0">
{formattedTimes[message.id] || ''}
</span>
<span className="text-muted-foreground">{message.message}</span>
</div>
)}
</div>
))}
</div>
</ScrollArea>
{showSettings && (
<div className="w-48 border-l border-border p-4 bg-muted/20">
<h4 className="font-semibold text-sm mb-3">Online Users</h4>
<UserList users={onlineUsers} />
</div>
)}
</div>
<div className="border-t border-border p-4">
<div className="flex gap-2">
<Input
value={inputMessage}
onChange={event => onInputChange(event.target.value)}
onKeyPress={onInputKeyPress}
placeholder="Type a message... (/help for commands)"
className="flex-1 font-mono"
/>
<Button onClick={onSendMessage} size="icon">
<PaperPlaneTilt size={18} />
</Button>
</div>
<p className="text-xs text-muted-foreground mt-2">
Press Enter to send. Type /help for commands.
</p>
</div>
</CardContent>
</Card>
)
}

View File

@@ -1,20 +0,0 @@
interface UserListProps {
users: string[]
}
export function UserList({ users }: UserListProps) {
if (users.length === 0) {
return <p className="text-sm text-muted-foreground">No users online</p>
}
return (
<div className="space-y-1.5 text-sm">
{users.map(username => (
<div key={username} className="flex items-center gap-2">
<div className="w-2 h-2 rounded-full bg-green-500" />
<span>{username}</span>
</div>
))}
</div>
)
}

View File

@@ -1,59 +0,0 @@
import { useEffect, useState } from 'react'
import type { ChatMessage } from './types'
type TimestampFormatter = (timestamp: number) => Promise<string> | string
export function useChatInput(onSubmit: () => void) {
const [inputMessage, setInputMessage] = useState('')
const handleKeyPress = (event: React.KeyboardEvent) => {
if (event.key === 'Enter' && !event.shiftKey) {
event.preventDefault()
onSubmit()
}
}
return {
inputMessage,
setInputMessage,
handleKeyPress,
}
}
export function useFormattedTimes(
messages: ChatMessage[] | undefined,
formatTime: TimestampFormatter
) {
const [formattedTimes, setFormattedTimes] = useState<Record<string, string>>({})
useEffect(() => {
let isMounted = true
const formatAllTimes = async () => {
if (!messages) {
setFormattedTimes({})
return
}
const entries = await Promise.all(
messages.map(async message => {
const formatted = await formatTime(message.timestamp)
return [message.id, formatted] as const
})
)
if (isMounted) {
setFormattedTimes(Object.fromEntries(entries))
}
}
formatAllTimes()
return () => {
isMounted = false
}
}, [messages, formatTime])
return formattedTimes
}

View File

@@ -1,10 +0,0 @@
export type ChatMessageType = 'message' | 'system' | 'join' | 'leave' | 'command'
export interface ChatMessage {
id: string
username: string
userId: string
message: string
timestamp: number
type: ChatMessageType
}

View File

@@ -105,7 +105,6 @@ export { FieldRenderer } from '../FieldRenderer'
export { GenericPage } from '../GenericPage'
export { GitHubActionsFetcher } from '../GitHubActionsFetcher'
export { GodCredentialsSettings } from '../GodCredentialsSettings'
export { IRCWebchatDeclarative } from '../misc/demos/IRCWebchatDeclarative'
export { JsonEditor } from '../JsonEditor'
export { ContactSection } from '../level1/ContactSection'
export { FeaturesSection } from '../level1/FeaturesSection'

View File

@@ -1,6 +1,5 @@
import type React from 'react'
import { IRCWebchatDeclarative } from '@/components/misc/demos/IRCWebchatDeclarative'
import { NotificationSummaryCard } from '@/components/NotificationSummaryCard'
import {
Alert,
@@ -39,21 +38,11 @@ export function RenderNode({ component, renderChildren, user }: RenderNodeProps)
const renderer = getDeclarativeRenderer()
if (renderer.hasComponentConfig(type)) {
if (type === 'IRCWebchat' && user) {
return (
<IRCWebchatDeclarative
user={user}
channelName={props.channelName || 'general'}
onClose={props.onClose}
/>
)
}
return (
<div className="p-4 border-2 border-dashed border-accent">
Declarative Component: {type}
Lua Package Component: {type}
<div className="text-xs text-muted-foreground mt-2">
This is a package-defined component
This component is rendered from packages/irc_webchat
</div>
</div>
)

View File

@@ -0,0 +1,136 @@
{
"id": "irc_webchat_layout",
"type": "Card",
"props": {
"className": "h-[500px] flex flex-col"
},
"children": [
{
"id": "header",
"type": "CardHeader",
"props": {
"className": "border-b pb-3"
},
"children": [
{
"id": "title_row",
"type": "Flex",
"props": {
"className": "items-center justify-between"
},
"children": [
{
"id": "channel_title",
"type": "Typography",
"props": {
"variant": "h6",
"text": "#{channelName}"
}
},
{
"id": "user_count",
"type": "Badge",
"props": {
"variant": "secondary",
"text": "{onlineUsersCount} online"
}
}
]
}
]
},
{
"id": "messages_area",
"type": "ScrollArea",
"props": {
"className": "flex-1 p-4"
},
"children": [
{
"id": "message_list",
"type": "Stack",
"props": {
"spacing": 1,
"dataSource": "messages"
},
"itemTemplate": {
"id": "message_item",
"type": "Box",
"props": {
"className": "font-mono text-sm"
},
"children": [
{
"id": "timestamp",
"type": "Typography",
"props": {
"variant": "caption",
"color": "text.secondary",
"text": "[{formattedTime}]"
}
},
{
"id": "username",
"type": "Typography",
"props": {
"variant": "body2",
"component": "span",
"fontWeight": "bold",
"text": " <{username}> "
}
},
{
"id": "message_text",
"type": "Typography",
"props": {
"variant": "body2",
"component": "span",
"text": "{message}"
}
}
]
}
}
]
},
{
"id": "input_area",
"type": "Box",
"props": {
"className": "border-t p-4"
},
"children": [
{
"id": "input_row",
"type": "Flex",
"props": {
"gap": 2
},
"children": [
{
"id": "message_input",
"type": "TextField",
"props": {
"fullWidth": true,
"size": "small",
"placeholder": "Type a message... (/help for commands)",
"value": "{inputMessage}",
"onKeyPress": "handleKeyPress",
"onChange": "updateInputMessage"
}
},
{
"id": "send_button",
"type": "Button",
"props": {
"variant": "contained",
"onClick": "handleSendMessage",
"text": "Send"
}
}
]
}
]
}
]
}

View File

@@ -9,6 +9,7 @@
"dependencies": [],
"exports": {
"components": ["IRCWebchat"],
"layouts": ["layout.json"],
"luaScripts": [
"send_message",
"handle_command",