Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
29 changes: 25 additions & 4 deletions src/main/api/bedrock/client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
import { BedrockAgentRuntimeClient } from '@aws-sdk/client-bedrock-agent-runtime'
import { TranslateClient } from '@aws-sdk/client-translate'
import { fromIni } from '@aws-sdk/credential-providers'
import { NodeHttpHandler } from '@smithy/node-http-handler'
import { NovaSonicBidirectionalStreamClient } from '../sonic/client'
import type { AWSCredentials } from './types'
import { S3Client } from '@aws-sdk/client-s3'
Expand Down Expand Up @@ -31,18 +32,28 @@
const { region, useProfile, profile, ...credentials } = awsCredentials
const httpOptions = createHttpOptions(awsCredentials)

// Add default timeout configuration if no custom handler is provided
const defaultHttpOptions = Object.keys(httpOptions).length === 0

Check warning on line 36 in src/main/api/bedrock/client.ts

View workflow job for this annotation

GitHub Actions / build (macos-latest)

Replace `·Object.keys(httpOptions).length·===·0·` with `⏎····Object.keys(httpOptions).length·===·0`
? {

Check warning on line 37 in src/main/api/bedrock/client.ts

View workflow job for this annotation

GitHub Actions / build (macos-latest)

Insert `··`
requestHandler: new NodeHttpHandler({

Check warning on line 38 in src/main/api/bedrock/client.ts

View workflow job for this annotation

GitHub Actions / build (macos-latest)

Insert `··`
requestTimeout: 300000, // 5 minutes for long-running operations

Check warning on line 39 in src/main/api/bedrock/client.ts

View workflow job for this annotation

GitHub Actions / build (macos-latest)

Replace `··········` with `············`
connectionTimeout: 30000 // 30 seconds for connection establishment

Check warning on line 40 in src/main/api/bedrock/client.ts

View workflow job for this annotation

GitHub Actions / build (macos-latest)

Insert `··`
})

Check warning on line 41 in src/main/api/bedrock/client.ts

View workflow job for this annotation

GitHub Actions / build (macos-latest)

Insert `··`
}

Check warning on line 42 in src/main/api/bedrock/client.ts

View workflow job for this annotation

GitHub Actions / build (macos-latest)

Insert `··`
: httpOptions

Check warning on line 43 in src/main/api/bedrock/client.ts

View workflow job for this annotation

GitHub Actions / build (macos-latest)

Insert `··`

if (useProfile) {
return new BedrockRuntimeClient({
region,
credentials: fromIni({ profile }),
...httpOptions
...defaultHttpOptions
})
}

return new BedrockRuntimeClient({
region,
credentials,
...httpOptions
...defaultHttpOptions
})
}

Expand All @@ -69,18 +80,28 @@
const { region, useProfile, profile, ...credentials } = awsCredentials
const httpOptions = createHttpOptions(awsCredentials)

// Add default timeout configuration if no custom handler is provided
const defaultHttpOptions = Object.keys(httpOptions).length === 0

Check warning on line 84 in src/main/api/bedrock/client.ts

View workflow job for this annotation

GitHub Actions / build (macos-latest)

Replace `·Object.keys(httpOptions).length·===·0·` with `⏎····Object.keys(httpOptions).length·===·0`
? {

Check warning on line 85 in src/main/api/bedrock/client.ts

View workflow job for this annotation

GitHub Actions / build (macos-latest)

Insert `··`
requestHandler: new NodeHttpHandler({
requestTimeout: 300000, // 5 minutes for long-running agent operations
connectionTimeout: 30000 // 30 seconds for connection establishment
})
}
: httpOptions

if (useProfile) {
return new BedrockAgentRuntimeClient({
region,
credentials: fromIni({ profile }),
...httpOptions
...defaultHttpOptions
})
}

return new BedrockAgentRuntimeClient({
region,
credentials,
...httpOptions
...defaultHttpOptions
})
}

Expand Down
4 changes: 3 additions & 1 deletion src/main/lib/proxy-utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -224,7 +224,9 @@ export function createProxyAgents(
if (options.includeNodeHandler) {
result.requestHandler = new NodeHttpHandler({
httpAgent: agent,
httpsAgent: agent
httpsAgent: agent,
requestTimeout: 300000, // 5 minutes
connectionTimeout: 30000 // 30 seconds
})
}

Expand Down
22 changes: 22 additions & 0 deletions src/renderer/src/contexts/SettingsContext.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,8 @@ export interface SettingsContextType {
updateContextLength: (length: number) => void
enablePromptCache: boolean
setEnablePromptCache: (enabled: boolean) => void
requestTimeout: number
setRequestTimeout: (timeout: number) => void

// Notification Settings
notification: boolean
Expand Down Expand Up @@ -290,6 +292,7 @@ export const SettingsProvider: React.FC<{ children: React.ReactNode }> = ({ chil
// Agent Chat Settings
const [contextLength, setContextLength] = useState<number>(60)
const [enablePromptCache, setStateEnablePromptCache] = useState<boolean>(true)
const [requestTimeout, setStateRequestTimeout] = useState<number>(15)

// Notification Settings
const [notification, setStateNotification] = useState<boolean>(true)
Expand Down Expand Up @@ -693,6 +696,14 @@ export const SettingsProvider: React.FC<{ children: React.ReactNode }> = ({ chil
// enablePromptCache の状態を更新
setStateEnablePromptCache(agentChatConfig.enablePromptCache)

// requestTimeout の設定
if (agentChatConfig.requestTimeout === undefined) {
agentChatConfig.requestTimeout = 15
}

// requestTimeout の状態を更新
setStateRequestTimeout(agentChatConfig.requestTimeout)

// 設定を保存
window.store.set('agentChatConfig', agentChatConfig)

Expand Down Expand Up @@ -1345,6 +1356,15 @@ export const SettingsProvider: React.FC<{ children: React.ReactNode }> = ({ chil
})
}, [])

const setRequestTimeout = useCallback((timeout: number) => {
setStateRequestTimeout(timeout)
const agentChatConfig = window.store.get('agentChatConfig') || {}
window.store.set('agentChatConfig', {
...agentChatConfig,
requestTimeout: timeout
})
}, [])

const setNotification = useCallback((enabled: boolean) => {
setStateNotification(enabled)
window.store.set('notification', enabled)
Expand Down Expand Up @@ -1842,6 +1862,8 @@ export const SettingsProvider: React.FC<{ children: React.ReactNode }> = ({ chil
updateContextLength,
enablePromptCache,
setEnablePromptCache,
requestTimeout,
setRequestTimeout,

// Notification Settings
notification,
Expand Down
19 changes: 18 additions & 1 deletion src/renderer/src/hooks/useChat.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ export const useChat = (props: UseChatProps) => {
const [loading, setLoading] = useState(false)
const [lastText, setLatestText] = useState('')
const [stopReason, setStopReason] = useState<StopReason>()
const [waitingForResponse, setWaitingForResponse] = useState(false)
const { t } = useTranslation()

const handleSubmit = useCallback(
Expand All @@ -26,6 +27,16 @@ export const useChat = (props: UseChatProps) => {
setMessages(msgs)

setLoading(true)
setWaitingForResponse(false)

// Track last data received time
let lastDataTime = Date.now()
const WAIT_THRESHOLD = 10000 // 10 seconds without data = waiting state

const checkWaitingState = setInterval(() => {
const timeSinceLastData = Date.now() - lastDataTime
setWaitingForResponse(timeSinceLastData > WAIT_THRESHOLD)
}, 1000)

const generator = streamChatCompletion({
messages: msgs,
Expand All @@ -40,6 +51,8 @@ export const useChat = (props: UseChatProps) => {
let s = ''
try {
for await (const json of generator) {
lastDataTime = Date.now() // Update last data time on each chunk

if (json.contentBlockDelta) {
const text = json.contentBlockDelta.delta?.text
if (text) {
Expand All @@ -60,9 +73,13 @@ export const useChat = (props: UseChatProps) => {
const msgsToset = [...msgs, { role: 'assistant', content: [{ text: error.message }] }]
setMessages(msgsToset)
setLoading(false)
setWaitingForResponse(false)
clearInterval(checkWaitingState)
}

clearInterval(checkWaitingState)
setLoading(false)
setWaitingForResponse(false)

const msgsToset = [...msgs, { role: 'assistant', content: [{ text: s }] }]
setMessages(msgsToset)
Expand All @@ -75,5 +92,5 @@ export const useChat = (props: UseChatProps) => {
setLatestText('')
}

return { messages, handleSubmit, loading, initChat, lastText, setLoading, stopReason }
return { messages, handleSubmit, loading, initChat, lastText, setLoading, stopReason, waitingForResponse }
}
6 changes: 6 additions & 0 deletions src/renderer/src/pages/ChatPage/ChatPage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,9 @@ export default function ChatPage() {
messages,
loading,
reasoning,
waitingForResponse,
timeoutCountdown,
heartbeatCount,
handleSubmit,
currentSessionId,
setCurrentSessionId,
Expand Down Expand Up @@ -293,6 +296,9 @@ export default function ChatPage() {
messages={messages}
loading={loading}
reasoning={reasoning}
waitingForResponse={waitingForResponse}
timeoutCountdown={timeoutCountdown}
heartbeatCount={heartbeatCount}
deleteMessage={handleDeleteMessage}
/>
</div>
Expand Down
47 changes: 44 additions & 3 deletions src/renderer/src/pages/ChatPage/components/MessageList/index.tsx
Original file line number Diff line number Diff line change
@@ -1,16 +1,35 @@
import { IdentifiableMessage } from '@/types/chat/message'
import React, { memo, useCallback } from 'react'
import React, { memo, useCallback, useEffect, useRef } from 'react'
import { ChatMessage } from './Message'
import AILogo from '@renderer/assets/images/icons/ai.svg'

type MessageListProps = {
messages: IdentifiableMessage[]
loading: boolean
reasoning: boolean
waitingForResponse?: boolean
timeoutCountdown?: number
heartbeatCount?: number
deleteMessage?: (index: number) => void
}

const LoadingMessage = memo(function LoadingMessage() {
const LoadingMessage = memo(function LoadingMessage({
waiting,
countdown,
heartbeats
}: {
waiting?: boolean
countdown?: number
heartbeats?: number
}) {
const formatTime = (seconds: number) => {
const mins = Math.floor(seconds / 60)
const secs = seconds % 60
return `${mins}:${secs.toString().padStart(2, '0')}`
}

const dots = heartbeats ? '..'.repeat(Math.min(heartbeats, 10)) : ''

return (
<div className="flex gap-4">
<div className="flex items-center justify-center w-10 h-10">
Expand All @@ -20,6 +39,11 @@ const LoadingMessage = memo(function LoadingMessage() {
</div>
<div className="flex flex-col gap-2 w-full">
<span className="animate-pulse h-2 w-12 bg-slate-200 rounded"></span>
{waiting && countdown !== undefined && (
<div className="text-xs text-amber-600 dark:text-amber-400 mb-2">
⏳ Processing... (Timeout in {formatTime(countdown)}){dots}
</div>
)}
<div className="flex-1 space-y-6 py-1">
<div className="space-y-3">
<div className="grid grid-cols-3 gap-4">
Expand All @@ -39,6 +63,9 @@ const MessageListBase: React.FC<MessageListProps> = ({
messages,
loading,
reasoning,
waitingForResponse,
timeoutCountdown,
heartbeatCount,
deleteMessage
}) => {
const handleDeleteMessage = useCallback(
Expand All @@ -50,6 +77,13 @@ const MessageListBase: React.FC<MessageListProps> = ({
[deleteMessage]
)

const messagesEndRef = useRef<HTMLDivElement>(null)

// Auto-scroll to bottom when messages change or loading state changes
useEffect(() => {
messagesEndRef.current?.scrollIntoView({ behavior: 'smooth' })
}, [messages, loading])

return (
<div className="flex flex-col gap-4">
{messages.map((message, index) => (
Expand All @@ -60,7 +94,14 @@ const MessageListBase: React.FC<MessageListProps> = ({
onDeleteMessage={deleteMessage ? handleDeleteMessage(index) : undefined}
/>
))}
{loading && <LoadingMessage />}
{loading && (
<LoadingMessage
waiting={waitingForResponse}
countdown={timeoutCountdown}
heartbeats={heartbeatCount}
/>
)}
<div ref={messagesEndRef} />
</div>
)
}
Expand Down
Loading
Loading