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
57 changes: 57 additions & 0 deletions app/Http/Controllers/ChatController.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
<?php

namespace App\Http\Controllers;

use App\Http\Requests\StoreWorkspaceChatRequest;
use App\Models\InstanceConnection;
use App\Models\WorkspaceChat;
use App\Support\Chats\WorkspaceChatManager;
use App\Support\Connections\InstanceConnectionManager;
use App\Support\Connections\ViewerIdentityResolver;
use Illuminate\Http\RedirectResponse;
use Illuminate\Http\Request;

class ChatController extends Controller
{
public function store(
StoreWorkspaceChatRequest $request,
InstanceConnectionManager $connectionManager,
ViewerIdentityResolver $viewerIdentityResolver,
WorkspaceChatManager $chatManager,
): RedirectResponse {
$activeConnection = $connectionManager->activeConnectionFor(
$request->user(),
$request->root(),
$request->session(),
);

if ($activeConnection->kind === InstanceConnection::KIND_SERVER && ! $activeConnection->is_authenticated) {
return to_route('connections.connect', $activeConnection);
}

$workspaces = $connectionManager->workspacesFor($activeConnection);
$activeWorkspace = $connectionManager->activeWorkspaceFor($activeConnection, $workspaces);
$viewerIdentity = $viewerIdentityResolver->resolve($request->user(), $activeConnection);

$chatManager->createChat($activeWorkspace, $request->user(), $viewerIdentity, [
'name' => $request->validated('chat_name'),
'kind' => $request->validated('chat_kind'),
]);

return to_route('home');
}

public function activate(
Request $request,
WorkspaceChat $workspaceChat,
WorkspaceChatManager $chatManager,
): RedirectResponse {
if ((int) $workspaceChat->workspace->instanceConnection->user_id !== (int) $request->user()->getKey()) {
abort(404);
}

$chatManager->activateChat($workspaceChat);

return to_route('home');
}
}
31 changes: 31 additions & 0 deletions app/Http/Controllers/ChatMessageController.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
<?php

namespace App\Http\Controllers;

use App\Http\Requests\StoreWorkspaceChatMessageRequest;
use App\Models\WorkspaceChat;
use App\Support\Chats\WorkspaceChatManager;
use App\Support\Connections\ViewerIdentityResolver;
use Illuminate\Http\RedirectResponse;

class ChatMessageController extends Controller
{
public function store(
StoreWorkspaceChatMessageRequest $request,
WorkspaceChat $workspaceChat,
ViewerIdentityResolver $viewerIdentityResolver,
WorkspaceChatManager $chatManager,
): RedirectResponse {
if ((int) $workspaceChat->workspace->instanceConnection->user_id !== (int) $request->user()->getKey()) {
abort(404);
}

$viewerIdentity = $viewerIdentityResolver->resolve($request->user(), $workspaceChat->workspace->instanceConnection);

$chatManager->createMessage($workspaceChat, $request->user(), $viewerIdentity, [
'body' => $request->validated('message_body'),
]);

return to_route('home');
}
}
174 changes: 64 additions & 110 deletions app/Http/Controllers/HomeController.php
Original file line number Diff line number Diff line change
Expand Up @@ -5,10 +5,14 @@
use App\Features\Desktop\MvpShell;
use App\Models\InstanceConnection;
use App\Models\SurrealWorkspace;
use App\Models\User;
use App\Models\Workspace;
use App\Models\WorkspaceChat;
use App\Models\WorkspaceChatMessage;
use App\Models\WorkspaceChatParticipant;
use App\Services\Surreal\SurrealRuntimeManager;
use App\Support\Chats\WorkspaceChatManager;
use App\Support\Connections\InstanceConnectionManager;
use App\Support\Connections\ViewerIdentityResolver;
use App\Support\Features\DesktopUi;
use Illuminate\Database\Eloquent\Collection as EloquentCollection;
use Illuminate\Http\RedirectResponse;
Expand Down Expand Up @@ -305,65 +309,63 @@ private function roomLinks(InstanceConnection $activeConnection, string $activeR
}

/**
* @param array<int, array{label: string, meta: string}> $participants
* @return array<int, array{label: string, active?: bool, prefix: string, tone: string}>
* @param EloquentCollection<int, WorkspaceChat> $chats
* @return array<int, array{label: string, active?: bool, prefix: string, tone: string, action?: string|null}>
*/
private function chatLinks(array $participants, string $viewerName): array
private function chatLinks(EloquentCollection $chats, WorkspaceChat $activeChat): array
{
$links = [
['label' => $viewerName, 'prefix' => substr($viewerName, 0, 1), 'tone' => 'human'],
];

foreach ($participants as $participant) {
if ($participant['meta'] === 'Human') {
continue;
}

$links[] = [
'label' => $participant['label'],
'prefix' => '@',
'tone' => 'bot',
];
}

return array_slice($links, 0, 3);
return $chats
->map(fn (WorkspaceChat $chat): array => [
'label' => $chat->name,
'active' => (int) $chat->getKey() === (int) $activeChat->getKey(),
'prefix' => $chat->kind === WorkspaceChat::KIND_DIRECT ? '@' : strtoupper(substr($chat->name, 0, 1)),
'tone' => $chat->kind === WorkspaceChat::KIND_DIRECT ? 'human' : 'room',
'action' => (int) $chat->getKey() === (int) $activeChat->getKey() ? null : route('chats.activate', $chat),
])
->values()
->all();
}

/**
* @param array<int, array{label: string, meta: string}> $participants
* @return array<int, array{
* label: string,
* value: string,
* prefix: string,
* tone: string,
* subtitle: string
* }>
* @return array<int, array{label: string, meta: string}>
*/
private function chatContacts(array $participants, string $viewerName): array
private function chatParticipants(WorkspaceChat $chat): array
{
$contacts = [[
'label' => $viewerName,
'value' => str($viewerName)->slug()->value(),
'prefix' => substr($viewerName, 0, 1),
'tone' => 'human',
'subtitle' => 'Human',
]];

foreach ($participants as $participant) {
if ($participant['meta'] === 'Human') {
continue;
}
return $chat->participants
->map(fn (WorkspaceChatParticipant $participant): array => [
'label' => $participant->display_name,
'meta' => $participant->participant_type === WorkspaceChatParticipant::TYPE_AGENT ? 'Agent' : 'Human',
])
->values()
->all();
}

$contacts[] = [
'label' => $participant['label'],
'value' => str($participant['label'])->slug()->value(),
'prefix' => '@',
'tone' => 'bot',
'subtitle' => $participant['meta'],
];
}
/**
* @return array<int, array{speaker: string, role: string, body: string, meta: string, tone: string}>
*/
private function chatMessages(WorkspaceChat $chat): array
{
return $chat->messages()
->orderBy('created_at')
->orderBy('id')
->get()
->map(function (WorkspaceChatMessage $message): array {
$role = match ($message->sender_type) {
WorkspaceChatMessage::SENDER_AGENT => 'Agent',
WorkspaceChatMessage::SENDER_SYSTEM => 'System',
default => 'Human',
};

return $contacts;
return [
'speaker' => $message->sender_name,
'role' => $role,
'body' => $message->body,
'meta' => $message->created_at?->diffForHumans() ?? 'Just now',
'tone' => $role === 'Human' ? 'plain' : 'accent',
];
})
->values()
->all();
}

/**
Expand Down Expand Up @@ -480,6 +482,8 @@ public function __invoke(
Request $request,
SurrealRuntimeManager $runtimeManager,
InstanceConnectionManager $connectionManager,
ViewerIdentityResolver $viewerIdentityResolver,
WorkspaceChatManager $chatManager,
): View|RedirectResponse {
$localReady = false;
$desktopUiStates = DesktopUi::states();
Expand All @@ -502,7 +506,7 @@ public function __invoke(
$request->session(),
);

$viewerIdentity = $this->viewerIdentity($request->user(), $activeConnection);
$viewerIdentity = $viewerIdentityResolver->resolve($request->user(), $activeConnection);
$viewerName = $viewerIdentity['name'];

if ($activeConnection->kind === InstanceConnection::KIND_SERVER && ! $activeConnection->is_authenticated) {
Expand All @@ -513,6 +517,10 @@ public function __invoke(
$workspaces = $connectionManager->workspacesFor($activeConnection);
$activeWorkspaceModel = $connectionManager->activeWorkspaceFor($activeConnection, $workspaces);
$activeWorkspace = $this->activeWorkspaceState($activeConnection, $activeWorkspaceModel, $localReady);
$activeChatModel = $chatManager->activeChatFor($activeWorkspaceModel, $request->user(), $viewerIdentity);
$chats = $chatManager->chatsFor($activeWorkspaceModel);
$participants = $this->chatParticipants($activeChatModel);
$messages = $this->chatMessages($activeChatModel);

return view('welcome', [
'mvpShellEnabled' => $mvpShellEnabled,
Expand All @@ -521,13 +529,13 @@ public function __invoke(
'favoritesEnabled' => self::FAVORITES_ENABLED,
'workspaceLinks' => $this->workspaceLinks($workspaces, $activeWorkspaceModel),
'activeWorkspace' => $activeWorkspace,
'activeChat' => $activeChatModel,
'favoriteLinks' => $this->favoriteLinks($activeWorkspace, $viewerName),
'roomLinks' => $this->roomLinks($activeConnection, $activeWorkspace['room']),
'chatLinks' => $this->chatLinks($activeWorkspace['participants'], $viewerName),
'chatContacts' => $this->chatContacts($activeWorkspace['participants'], $viewerName),
'chatLinks' => $this->chatLinks($chats, $activeChatModel),
'conversationNodeTabs' => $this->conversationNodeTabs($activeWorkspace),
'messages' => $activeWorkspace['messages'],
'participants' => $activeWorkspace['participants'],
'messages' => $messages,
'participants' => $participants,
'viewerName' => $viewerIdentity['name'],
'viewerEmail' => $viewerIdentity['email'],
'viewerInitials' => $viewerIdentity['initials'],
Expand All @@ -551,58 +559,4 @@ private function connectionPrefix(InstanceConnection $connection): string
{
return strtoupper(substr($connection->name, 0, 1));
}

/**
* @return array{name: string, email: string, initials: string}
*/
private function viewerIdentity(?User $viewer, InstanceConnection $activeConnection): array
{
$remoteIdentity = $activeConnection->kind === InstanceConnection::KIND_SERVER
? data_get($activeConnection->session_context, 'user')
: null;
$remoteEmail = $activeConnection->kind === InstanceConnection::KIND_SERVER
? data_get($activeConnection->session_context, 'email')
: null;
$remoteName = data_get($remoteIdentity, 'name');

if ((! is_string($remoteName) || $remoteName === '') && is_string($remoteEmail) && $remoteEmail !== '') {
$remoteName = $this->nameFromEmail($remoteEmail);
}

$name = $remoteName
?: $viewer?->name
?: 'Derek Bourgeois';

$email = data_get($remoteIdentity, 'email')
?: $remoteEmail
?: $viewer?->email
?: 'derek@katra.io';

$initials = collect(preg_split('/\s+/', trim($name)) ?: [])
->filter()
->take(2)
->map(fn (string $segment): string => strtoupper(substr($segment, 0, 1)))
->implode('');

return [
'name' => $name,
'email' => $email,
'initials' => $initials !== '' ? $initials : 'K',
];
}

private function nameFromEmail(string $email): string
{
$localPart = (string) str($email)->before('@');
$segments = preg_split('/[._-]+/', $localPart) ?: [];
$segments = array_values(array_filter(array_map(
fn (string $segment): string => str($segment)->title()->value(),
$segments,
)));

$firstName = $segments[0] ?? 'Remote';
$lastName = count($segments) > 1 ? implode(' ', array_slice($segments, 1)) : 'User';

return trim($firstName.' '.$lastName);
}
}
35 changes: 35 additions & 0 deletions app/Http/Requests/StoreWorkspaceChatMessageRequest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
<?php

namespace App\Http\Requests;

use Illuminate\Contracts\Validation\ValidationRule;
use Illuminate\Foundation\Http\FormRequest;

class StoreWorkspaceChatMessageRequest extends FormRequest
{
public function authorize(): bool
{
return $this->user() !== null;
}

/**
* @return array<string, ValidationRule|array<int, ValidationRule|string>|string>
*/
public function rules(): array
{
return [
'message_body' => ['required', 'string', 'max:10000', 'regex:/\\S/'],
];
}

/**
* @return array<string, string>
*/
public function messages(): array
{
return [
'message_body.required' => 'Enter a message before sending it.',
'message_body.regex' => 'Messages cannot be blank.',
];
}
}
Loading
Loading