eyeloupe 0.2.0 → 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (38) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +9 -3
  3. data/README.md +36 -7
  4. data/app/assets/builds/eyeloupe.css +1 -1
  5. data/app/assets/javascripts/eyeloupe/controllers/eyeloupe/ai_assistant_controller.js +27 -0
  6. data/app/assets/javascripts/eyeloupe/controllers/eyeloupe/refresh_controller.js +3 -1
  7. data/app/assets/stylesheets/application.tailwind.css +8 -0
  8. data/app/assets/stylesheets/eyeloupe/application.css +0 -1
  9. data/app/controllers/eyeloupe/ai_assistant_responses_controller.rb +30 -0
  10. data/app/controllers/eyeloupe/data_controller.rb +2 -0
  11. data/app/controllers/eyeloupe/exceptions_controller.rb +29 -0
  12. data/app/models/eyeloupe/exception.rb +6 -0
  13. data/app/models/eyeloupe/in_request.rb +1 -0
  14. data/app/models/eyeloupe/out_request.rb +1 -0
  15. data/app/views/eyeloupe/exceptions/_frame.html.erb +39 -0
  16. data/app/views/eyeloupe/exceptions/index.html.erb +13 -0
  17. data/app/views/eyeloupe/exceptions/show.html.erb +126 -0
  18. data/app/views/eyeloupe/in_requests/_frame.html.erb +1 -1
  19. data/app/views/eyeloupe/in_requests/index.html.erb +1 -1
  20. data/app/views/eyeloupe/in_requests/show.html.erb +17 -1
  21. data/app/views/eyeloupe/out_requests/_frame.html.erb +1 -1
  22. data/app/views/eyeloupe/out_requests/index.html.erb +1 -1
  23. data/app/views/eyeloupe/out_requests/show.html.erb +17 -1
  24. data/app/views/layouts/eyeloupe/application.html.erb +41 -5
  25. data/config/importmap.rb +2 -1
  26. data/config/routes.rb +2 -0
  27. data/db/migrate/20230604190442_create_eyeloupe_exceptions.rb +21 -0
  28. data/lib/eyeloupe/concerns/rescuable.rb +14 -0
  29. data/lib/eyeloupe/configuration.rb +7 -0
  30. data/lib/eyeloupe/engine.rb +46 -0
  31. data/lib/eyeloupe/processors/exception.rb +71 -0
  32. data/lib/eyeloupe/processors/in_request.rb +14 -3
  33. data/lib/eyeloupe/processors/out_request.rb +10 -20
  34. data/lib/eyeloupe/request_middleware.rb +13 -1
  35. data/lib/eyeloupe/version.rb +1 -1
  36. data/lib/eyeloupe.rb +3 -1
  37. metadata +26 -3
  38. data/lib/eyeloupe/http.rb +0 -19
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 7094456114ea58b40fb510872bbcf8fc980bcc3140b172ff453e12126b0b7fec
4
- data.tar.gz: fc95215a4ed95e7ad5afadce42d809c2cf216b29ca977c16085fc1a1e5d83707
3
+ metadata.gz: 5a2fc3039c5a9cdcbd79a0a5639b74c52c3fc42f0c96fc569f92ce38255e7768
4
+ data.tar.gz: 4de09b66849266fa6e43af59a55a60ab60464bb1310f8dff5a28a8e84904dac9
5
5
  SHA512:
6
- metadata.gz: 9544ddcf4d4e553ab4419c29b0e5108ba19df99f653807c1ce13c80db4b98afbd4fc0a178f2ae49efafb81418c2df2a49b8d09a623bc219afb7d89bedd540e55
7
- data.tar.gz: b368ac6289aad17ea863b7b348940050db7dab36c0e50179375c64ede9feb059af5208cdb789b770ae038bc79d616f04477cb3e584c954ac7ba4addf7fba07ca
6
+ metadata.gz: 0c02695f8f4f26d09df3fda942691bf6729c9c1774b95acc1e9663e48c0ae25bad3d6991804a94163ff6595c8dbc8e56cb7f6dbd6f0be6eb195a2332446b5d9d
7
+ data.tar.gz: b61c44badf4b0713186338a76d0a246c1f87586d88e8404861931b326aafcfc8945977222e2296fc3d10aee2eb2f369df99cdcc84d4a8c9bb508453f579a6927
data/CHANGELOG.md CHANGED
@@ -1,7 +1,13 @@
1
- ## 0.1.0
1
+ ## 0.3.0
2
2
 
3
- - Initial release including incoming and outgoing requests.
3
+ - Add exceptions: Framework + ActiveJob + Sidekiq Worker exceptions.
4
+ - Add OpenAI support for AI assistant in exceptions.
5
+ - Fix exceptions when using importmap binary by adding net/http override in railties initializer.
4
6
 
5
7
  ## 0.2.0
6
8
 
7
- - Fix missing require for `pagy` gem.
9
+ - Fix missing require for `pagy` gem.
10
+
11
+ ## 0.1.0
12
+
13
+ - Initial release including incoming and outgoing requests.
data/README.md CHANGED
@@ -1,4 +1,4 @@
1
- [![Gem Version][gem-version]][gem-url]
1
+ [![Gem Version](https://badge.fury.io/rb/eyeloupe.svg)](https://badge.fury.io/rb/eyeloupe)
2
2
 
3
3
  [![Contributors][contributors-shield]][contributors-url]
4
4
  [![Forks][forks-shield]][forks-url]
@@ -12,10 +12,10 @@
12
12
  <img src="app/assets/images/eyeloupe/logo.png" width=120 alt="Logo" >
13
13
  </a>
14
14
 
15
- <h3 align="center">Eyeloupe</h3>
15
+ <h3 align="center">Eyeloupe (beta)</h3>
16
16
 
17
17
  <p align="center">
18
- The elegant Rails debug assistant.
18
+ The elegant Rails debug assistant. AI powered.
19
19
  <br />
20
20
  <a href="https://github.com/alxlion/eyeloupe/issues">Report Bug</a>
21
21
  ·
@@ -25,7 +25,7 @@
25
25
 
26
26
  [![Eyeloupe screenshot][eyeloupe-screen]](https://github.com/alxlion/eyeloupe)
27
27
 
28
- Eyeloupe is the elegant Rails debug assistant. It helps you to debug your Rails application by providing a simple and elegant interface to view your incoming and outgoing requests (and a lot more to come).
28
+ Eyeloupe is the elegant Rails debug assistant. It helps you to debug your Rails application by providing a simple and elegant interface to view your incoming/outgoing requests and exceptions, powered by AI.
29
29
 
30
30
  ## Installation
31
31
  Add this line to your application's Gemfile:
@@ -61,12 +61,24 @@ This is an example of the configuration you can add to your `initializers/eyelou
61
61
  ```ruby
62
62
  Eyeloupe.configure do |config|
63
63
  config.excluded_paths = %w[assets favicon.ico service-worker.js manifest.json]
64
- config.capture = true
64
+ config.capture = Rails.env.development?
65
+ config.openai_access_key = "your-openai-access-key"
66
+ config.openai_model = "gpt-4"
65
67
  end
66
68
  ```
67
69
 
68
70
  - `excluded_paths` is an array of paths you want to exclude from Eyeloupe capture. Eyeloupe adds these excluded paths to the default ones: ` %w[mini-profiler eyeloupe active_storage]`
69
71
  - `capture` is a boolean to enable/disable Eyeloupe capture. By default, it's set to `true`.
72
+ - `openai_access_key` is the access key to use the OpenAI API. You can get one [here](https://platform.openai.com/).
73
+ - `openai_model` is the model to use for the OpenAI API. You can find the list of available models [here](https://platform.openai.com/docs/models).
74
+
75
+ ### Exception handling
76
+
77
+ To be able to handle exceptions, be sure to disable the default Rails exception handling in your environment config file (e.g. `config/environments/development.rb`):
78
+
79
+ ```ruby
80
+ config.consider_all_requests_local = false
81
+ ```
70
82
 
71
83
  ## Usage
72
84
 
@@ -82,6 +94,21 @@ By activating auto-fresh, every _3 seconds_ the page will be refreshed to show y
82
94
 
83
95
  You can delete all the data stored by Eyeloupe by clicking on the trash button.
84
96
 
97
+ ### AI Assistant
98
+
99
+ When you define an OpenAI access key in the configuration, you could see a new section in the exception details page. This section is powered by the OpenAI API and it's able to give you a solution to solve your exception.
100
+ It sends the entire content of the file containing the exception to have the best answer to your problem.
101
+
102
+ ![Eyeloupe ai_assistant](/doc/img/ai-assistant.gif)
103
+
104
+ ## Upgrade
105
+
106
+ When your upgrade Eyeloupe to the latest version, be sure to run the following commands:
107
+
108
+ ```bash
109
+ $ rails eyeloupe:install:migrations
110
+ $ rails db:migrate
111
+ ```
85
112
 
86
113
  ## Q/A
87
114
 
@@ -95,8 +122,10 @@ Yes, Eyeloupe is inspired by Laravel Telescope. A lot of people coming from Lara
95
122
 
96
123
  ## Roadmap
97
124
 
98
- - [ ] Exceptions - Track all the exceptions thrown by your application
125
+ - [x] Exceptions - Track all the exceptions thrown by your application
126
+ - [x] AI assistant - Use OpenAI API to help you to solve your exceptions
99
127
  - [ ] Custom links to the menu - To access all of your debug tool in one place (Sidekiq web, Mailhog, etc.)
128
+ - [ ] Refactoring / clean code - To make the code more readable and maintainable
100
129
 
101
130
  ## Contributing
102
131
  Contributions are what makes the open-source community such an amazing place to learn, inspire, and create. Any contributions you make are **greatly appreciated**.
@@ -135,4 +164,4 @@ Project Link: [https://github.com/alxlion/eyeloupe](https://github.com/alxlion/e
135
164
  [license-url]: https://github.com/alxlion/eyeloupe/blob/master/MIT-LICENSE.txt
136
165
  [eyeloupe-screen]: /doc/img/screen.png
137
166
  [gem-version]: https://badge.fury.io/rb/eyeloupe.svg
138
- [gem-url]: https://rubygems.org/gems/eyeloupe
167
+ [gem-url]: https://rubygems.org/gems/eyeloupe
@@ -1 +1 @@
1
- /*! tailwindcss v3.1.3 | MIT License | https://tailwindcss.com*/*,:after,:before{border:0 solid #e5e7eb;box-sizing:border-box}:after,:before{--tw-content:""}html{-webkit-text-size-adjust:100%;font-family:Fira Sans,sans-serif,ui-sans-serif,system-ui,-apple-system,BlinkMacSystemFont,Segoe UI,Roboto,Helvetica Neue,Arial,Noto Sans,Apple Color Emoji,Segoe UI Emoji,Segoe UI Symbol,Noto Color Emoji;line-height:1.5;-moz-tab-size:4;-o-tab-size:4;tab-size:4}body{line-height:inherit;margin:0}hr{border-top-width:1px;color:inherit;height:0}abbr:where([title]){-webkit-text-decoration:underline dotted;text-decoration:underline dotted}h1,h2,h3,h4,h5,h6{font-size:inherit;font-weight:inherit}a{color:inherit;text-decoration:inherit}b,strong{font-weight:bolder}code,kbd,pre,samp{font-family:ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,Liberation Mono,Courier New,monospace;font-size:1em}small{font-size:80%}sub,sup{font-size:75%;line-height:0;position:relative;vertical-align:initial}sub{bottom:-.25em}sup{top:-.5em}table{border-collapse:collapse;border-color:inherit;text-indent:0}button,input,optgroup,select,textarea{color:inherit;font-family:inherit;font-size:100%;font-weight:inherit;line-height:inherit;margin:0;padding:0}button,select{text-transform:none}[type=button],[type=reset],[type=submit],button{-webkit-appearance:button;background-color:initial;background-image:none}:-moz-focusring{outline:auto}:-moz-ui-invalid{box-shadow:none}progress{vertical-align:initial}::-webkit-inner-spin-button,::-webkit-outer-spin-button{height:auto}[type=search]{-webkit-appearance:textfield;outline-offset:-2px}::-webkit-search-decoration{-webkit-appearance:none}::-webkit-file-upload-button{-webkit-appearance:button;font:inherit}summary{display:list-item}blockquote,dd,dl,figure,h1,h2,h3,h4,h5,h6,hr,p,pre{margin:0}fieldset{margin:0}fieldset,legend{padding:0}menu,ol,ul{list-style:none;margin:0;padding:0}textarea{resize:vertical}input::-moz-placeholder,textarea::-moz-placeholder{color:#9ca3af;opacity:1}input:-ms-input-placeholder,textarea:-ms-input-placeholder{color:#9ca3af;opacity:1}input::placeholder,textarea::placeholder{color:#9ca3af;opacity:1}[role=button],button{cursor:pointer}:disabled{cursor:default}audio,canvas,embed,iframe,img,object,svg,video{display:block;vertical-align:middle}img,video{height:auto;max-width:100%}[multiple],[type=date],[type=datetime-local],[type=email],[type=month],[type=number],[type=password],[type=search],[type=tel],[type=text],[type=time],[type=url],[type=week],select,textarea{--tw-shadow:0 0 #0000;-webkit-appearance:none;-moz-appearance:none;appearance:none;background-color:#fff;border-color:#6b7280;border-radius:0;border-width:1px;font-size:1rem;line-height:1.5rem;padding:.5rem .75rem}[multiple]:focus,[type=date]:focus,[type=datetime-local]:focus,[type=email]:focus,[type=month]:focus,[type=number]:focus,[type=password]:focus,[type=search]:focus,[type=tel]:focus,[type=text]:focus,[type=time]:focus,[type=url]:focus,[type=week]:focus,select:focus,textarea:focus{--tw-ring-inset:var(--tw-empty,/*!*/ /*!*/);--tw-ring-offset-width:0px;--tw-ring-offset-color:#fff;--tw-ring-color:#2563eb;--tw-ring-offset-shadow:var(--tw-ring-inset) 0 0 0 var(--tw-ring-offset-width) var(--tw-ring-offset-color);--tw-ring-shadow:var(--tw-ring-inset) 0 0 0 calc(1px + var(--tw-ring-offset-width)) var(--tw-ring-color);border-color:#2563eb;box-shadow:var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow);outline:2px solid #0000;outline-offset:2px}input::-moz-placeholder,textarea::-moz-placeholder{color:#6b7280;opacity:1}input:-ms-input-placeholder,textarea:-ms-input-placeholder{color:#6b7280;opacity:1}input::placeholder,textarea::placeholder{color:#6b7280;opacity:1}::-webkit-datetime-edit-fields-wrapper{padding:0}::-webkit-date-and-time-value{min-height:1.5em}select{color-adjust:exact;background-image:url("data:image/svg+xml;charset=utf-8,%3Csvg xmlns='http://www.w3.org/2000/svg' fill='none' viewBox='0 0 20 20'%3E%3Cpath stroke='%236b7280' stroke-linecap='round' stroke-linejoin='round' stroke-width='1.5' d='m6 8 4 4 4-4'/%3E%3C/svg%3E");background-position:right .5rem center;background-repeat:no-repeat;background-size:1.5em 1.5em;padding-right:2.5rem;-webkit-print-color-adjust:exact}[multiple]{color-adjust:unset;background-image:none;background-position:0 0;background-repeat:unset;background-size:initial;padding-right:.75rem;-webkit-print-color-adjust:unset}[type=checkbox],[type=radio]{color-adjust:exact;--tw-shadow:0 0 #0000;-webkit-appearance:none;-moz-appearance:none;appearance:none;background-color:#fff;background-origin:border-box;border-color:#6b7280;border-width:1px;color:#2563eb;display:inline-block;flex-shrink:0;height:1rem;padding:0;-webkit-print-color-adjust:exact;-webkit-user-select:none;-moz-user-select:none;-ms-user-select:none;user-select:none;vertical-align:middle;width:1rem}[type=checkbox]{border-radius:0}[type=radio]{border-radius:100%}[type=checkbox]:focus,[type=radio]:focus{--tw-ring-inset:var(--tw-empty,/*!*/ /*!*/);--tw-ring-offset-width:2px;--tw-ring-offset-color:#fff;--tw-ring-color:#2563eb;--tw-ring-offset-shadow:var(--tw-ring-inset) 0 0 0 var(--tw-ring-offset-width) var(--tw-ring-offset-color);--tw-ring-shadow:var(--tw-ring-inset) 0 0 0 calc(2px + var(--tw-ring-offset-width)) var(--tw-ring-color);box-shadow:var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow);outline:2px solid #0000;outline-offset:2px}[type=checkbox]:checked,[type=radio]:checked{background-color:currentColor;background-position:50%;background-repeat:no-repeat;background-size:100% 100%;border-color:#0000}[type=checkbox]:checked{background-image:url("data:image/svg+xml;charset=utf-8,%3Csvg viewBox='0 0 16 16' fill='%23fff' xmlns='http://www.w3.org/2000/svg'%3E%3Cpath d='M12.207 4.793a1 1 0 0 1 0 1.414l-5 5a1 1 0 0 1-1.414 0l-2-2a1 1 0 0 1 1.414-1.414L6.5 9.086l4.293-4.293a1 1 0 0 1 1.414 0z'/%3E%3C/svg%3E")}[type=radio]:checked{background-image:url("data:image/svg+xml;charset=utf-8,%3Csvg viewBox='0 0 16 16' fill='%23fff' xmlns='http://www.w3.org/2000/svg'%3E%3Ccircle cx='8' cy='8' r='3'/%3E%3C/svg%3E")}[type=checkbox]:checked:focus,[type=checkbox]:checked:hover,[type=checkbox]:indeterminate,[type=radio]:checked:focus,[type=radio]:checked:hover{background-color:currentColor;border-color:#0000}[type=checkbox]:indeterminate{background-image:url("data:image/svg+xml;charset=utf-8,%3Csvg xmlns='http://www.w3.org/2000/svg' fill='none' viewBox='0 0 16 16'%3E%3Cpath stroke='%23fff' stroke-linecap='round' stroke-linejoin='round' stroke-width='2' d='M4 8h8'/%3E%3C/svg%3E");background-position:50%;background-repeat:no-repeat;background-size:100% 100%}[type=checkbox]:indeterminate:focus,[type=checkbox]:indeterminate:hover{background-color:currentColor;border-color:#0000}[type=file]{background:unset;border-color:inherit;border-radius:0;border-width:0;font-size:unset;line-height:inherit;padding:0}[type=file]:focus{outline:1px auto -webkit-focus-ring-color}*,:after,:before{--tw-border-spacing-x:0;--tw-border-spacing-y:0;--tw-translate-x:0;--tw-translate-y:0;--tw-rotate:0;--tw-skew-x:0;--tw-skew-y:0;--tw-scale-x:1;--tw-scale-y:1;--tw-pan-x: ;--tw-pan-y: ;--tw-pinch-zoom: ;--tw-scroll-snap-strictness:proximity;--tw-ordinal: ;--tw-slashed-zero: ;--tw-numeric-figure: ;--tw-numeric-spacing: ;--tw-numeric-fraction: ;--tw-ring-inset: ;--tw-ring-offset-width:0px;--tw-ring-offset-color:#fff;--tw-ring-color:#3b82f680;--tw-ring-offset-shadow:0 0 #0000;--tw-ring-shadow:0 0 #0000;--tw-shadow:0 0 #0000;--tw-shadow-colored:0 0 #0000;--tw-blur: ;--tw-brightness: ;--tw-contrast: ;--tw-grayscale: ;--tw-hue-rotate: ;--tw-invert: ;--tw-saturate: ;--tw-sepia: ;--tw-drop-shadow: ;--tw-backdrop-blur: ;--tw-backdrop-brightness: ;--tw-backdrop-contrast: ;--tw-backdrop-grayscale: ;--tw-backdrop-hue-rotate: ;--tw-backdrop-invert: ;--tw-backdrop-opacity: ;--tw-backdrop-saturate: ;--tw-backdrop-sepia: }::-webkit-backdrop{--tw-border-spacing-x:0;--tw-border-spacing-y:0;--tw-translate-x:0;--tw-translate-y:0;--tw-rotate:0;--tw-skew-x:0;--tw-skew-y:0;--tw-scale-x:1;--tw-scale-y:1;--tw-pan-x: ;--tw-pan-y: ;--tw-pinch-zoom: ;--tw-scroll-snap-strictness:proximity;--tw-ordinal: ;--tw-slashed-zero: ;--tw-numeric-figure: ;--tw-numeric-spacing: ;--tw-numeric-fraction: ;--tw-ring-inset: ;--tw-ring-offset-width:0px;--tw-ring-offset-color:#fff;--tw-ring-color:#3b82f680;--tw-ring-offset-shadow:0 0 #0000;--tw-ring-shadow:0 0 #0000;--tw-shadow:0 0 #0000;--tw-shadow-colored:0 0 #0000;--tw-blur: ;--tw-brightness: ;--tw-contrast: ;--tw-grayscale: ;--tw-hue-rotate: ;--tw-invert: ;--tw-saturate: ;--tw-sepia: ;--tw-drop-shadow: ;--tw-backdrop-blur: ;--tw-backdrop-brightness: ;--tw-backdrop-contrast: ;--tw-backdrop-grayscale: ;--tw-backdrop-hue-rotate: ;--tw-backdrop-invert: ;--tw-backdrop-opacity: ;--tw-backdrop-saturate: ;--tw-backdrop-sepia: }::backdrop{--tw-border-spacing-x:0;--tw-border-spacing-y:0;--tw-translate-x:0;--tw-translate-y:0;--tw-rotate:0;--tw-skew-x:0;--tw-skew-y:0;--tw-scale-x:1;--tw-scale-y:1;--tw-pan-x: ;--tw-pan-y: ;--tw-pinch-zoom: ;--tw-scroll-snap-strictness:proximity;--tw-ordinal: ;--tw-slashed-zero: ;--tw-numeric-figure: ;--tw-numeric-spacing: ;--tw-numeric-fraction: ;--tw-ring-inset: ;--tw-ring-offset-width:0px;--tw-ring-offset-color:#fff;--tw-ring-color:#3b82f680;--tw-ring-offset-shadow:0 0 #0000;--tw-ring-shadow:0 0 #0000;--tw-shadow:0 0 #0000;--tw-shadow-colored:0 0 #0000;--tw-blur: ;--tw-brightness: ;--tw-contrast: ;--tw-grayscale: ;--tw-hue-rotate: ;--tw-invert: ;--tw-saturate: ;--tw-sepia: ;--tw-drop-shadow: ;--tw-backdrop-blur: ;--tw-backdrop-brightness: ;--tw-backdrop-contrast: ;--tw-backdrop-grayscale: ;--tw-backdrop-hue-rotate: ;--tw-backdrop-invert: ;--tw-backdrop-opacity: ;--tw-backdrop-saturate: ;--tw-backdrop-sepia: }.sr-only{clip:rect(0,0,0,0);border-width:0;height:1px;margin:-1px;overflow:hidden;padding:0;position:absolute;white-space:nowrap;width:1px}.static{position:static}.fixed{position:fixed}.absolute{position:absolute}.relative{position:relative}.sticky{position:-webkit-sticky;position:sticky}.inset-0{bottom:0;left:0;right:0;top:0}.left-full{left:100%}.top-0{top:0}.z-50{z-index:50}.z-40{z-index:40}.-m-2\.5{margin:-.625rem}.-m-2{margin:-.5rem}.-mx-4{margin-left:-1rem;margin-right:-1rem}.-my-2{margin-bottom:-.5rem;margin-top:-.5rem}.-mx-2{margin-left:-.5rem;margin-right:-.5rem}.mx-auto{margin-left:auto;margin-right:auto}.mt-4{margin-top:1rem}.mt-2{margin-top:.5rem}.mt-8{margin-top:2rem}.mt-6{margin-top:1.5rem}.mt-1{margin-top:.25rem}.mr-16{margin-right:4rem}.ml-2{margin-left:.5rem}.inline-block{display:inline-block}.flex{display:flex}.inline-flex{display:inline-flex}.table{display:table}.flow-root{display:flow-root}.hidden{display:none}.h-6{height:1.5rem}.h-16{height:4rem}.h-7{height:1.75rem}.h-12{height:3rem}.h-5{height:1.25rem}.w-full{width:100%}.w-16{width:4rem}.w-6{width:1.5rem}.w-auto{width:auto}.w-5{width:1.25rem}.min-w-full{min-width:100%}.max-w-xs{max-width:20rem}.flex-1{flex:1 1 0%}.shrink-0{flex-shrink:0}.grow{flex-grow:1}.flex-col{flex-direction:column}.items-end{align-items:flex-end}.items-center{align-items:center}.justify-end{justify-content:flex-end}.justify-center{justify-content:center}.justify-between{justify-content:space-between}.gap-y-5{row-gap:1.25rem}.gap-y-7{row-gap:1.75rem}.gap-x-3{-moz-column-gap:.75rem;column-gap:.75rem}.gap-x-2{-moz-column-gap:.5rem;column-gap:.5rem}.gap-x-6{-moz-column-gap:1.5rem;column-gap:1.5rem}.gap-x-1{-moz-column-gap:.25rem;column-gap:.25rem}.space-y-1>:not([hidden])~:not([hidden]){--tw-space-y-reverse:0;margin-bottom:calc(.25rem*var(--tw-space-y-reverse));margin-top:calc(.25rem*(1 - var(--tw-space-y-reverse)))}.divide-y>:not([hidden])~:not([hidden]){--tw-divide-y-reverse:0;border-bottom-width:calc(1px*var(--tw-divide-y-reverse));border-top-width:calc(1px*(1 - var(--tw-divide-y-reverse)))}.divide-gray-300>:not([hidden])~:not([hidden]){--tw-divide-opacity:1;border-color:rgb(209 213 219/var(--tw-divide-opacity))}.divide-gray-200>:not([hidden])~:not([hidden]){--tw-divide-opacity:1;border-color:rgb(229 231 235/var(--tw-divide-opacity))}.divide-gray-100>:not([hidden])~:not([hidden]){--tw-divide-opacity:1;border-color:rgb(243 244 246/var(--tw-divide-opacity))}.overflow-x-auto{overflow-x:auto}.overflow-y-auto{overflow-y:auto}.truncate{overflow:hidden;text-overflow:ellipsis}.truncate,.whitespace-nowrap{white-space:nowrap}.rounded-md{border-radius:.375rem}.border{border-width:1px}.border-t{border-top-width:1px}.border-gray-300{--tw-border-opacity:1;border-color:rgb(209 213 219/var(--tw-border-opacity))}.border-gray-100{--tw-border-opacity:1;border-color:rgb(243 244 246/var(--tw-border-opacity))}.bg-red-500{--tw-bg-opacity:1;background-color:rgb(239 68 68/var(--tw-bg-opacity))}.bg-gray-200{--tw-bg-opacity:1;background-color:rgb(229 231 235/var(--tw-bg-opacity))}.bg-black{--tw-bg-opacity:1;background-color:rgb(0 0 0/var(--tw-bg-opacity))}.bg-green-50{--tw-bg-opacity:1;background-color:rgb(240 253 244/var(--tw-bg-opacity))}.bg-blue-50{--tw-bg-opacity:1;background-color:rgb(239 246 255/var(--tw-bg-opacity))}.bg-yellow-50{--tw-bg-opacity:1;background-color:rgb(254 252 232/var(--tw-bg-opacity))}.bg-red-50{--tw-bg-opacity:1;background-color:rgb(254 242 242/var(--tw-bg-opacity))}.bg-gray-50{--tw-bg-opacity:1;background-color:rgb(249 250 251/var(--tw-bg-opacity))}.bg-purple-50{--tw-bg-opacity:1;background-color:rgb(250 245 255/var(--tw-bg-opacity))}.bg-gray-900\/80{background-color:#111827cc}.bg-white{--tw-bg-opacity:1;background-color:rgb(255 255 255/var(--tw-bg-opacity))}.p-2{padding:.5rem}.p-2\.5{padding:.625rem}.p-1{padding:.25rem}.py-2{padding-bottom:.5rem;padding-top:.5rem}.py-3{padding-bottom:.75rem;padding-top:.75rem}.px-3{padding-left:.75rem;padding-right:.75rem}.py-4{padding-bottom:1rem;padding-top:1rem}.px-4{padding-left:1rem;padding-right:1rem}.py-6{padding-bottom:1.5rem;padding-top:1.5rem}.px-2{padding-left:.5rem;padding-right:.5rem}.py-1{padding-bottom:.25rem;padding-top:.25rem}.px-6{padding-left:1.5rem;padding-right:1.5rem}.py-10{padding-bottom:2.5rem;padding-top:2.5rem}.px-5{padding-left:1.25rem;padding-right:1.25rem}.py-5{padding-bottom:1.25rem;padding-top:1.25rem}.pl-4{padding-left:1rem}.pr-3{padding-right:.75rem}.pl-3{padding-left:.75rem}.pr-4{padding-right:1rem}.pt-5{padding-top:1.25rem}.pb-2{padding-bottom:.5rem}.text-left{text-align:left}.text-right{text-align:right}.align-middle{vertical-align:middle}.text-sm{font-size:.875rem;line-height:1.25rem}.text-base{font-size:1rem;line-height:1.5rem}.text-xl{font-size:1.25rem;line-height:1.75rem}.text-2xl{font-size:1.5rem;line-height:2rem}.font-medium{font-weight:500}.font-semibold{font-weight:600}.uppercase{text-transform:uppercase}.leading-6{line-height:1.5rem}.leading-7{line-height:1.75rem}.tracking-wide{letter-spacing:.025em}.text-white{--tw-text-opacity:1;color:rgb(255 255 255/var(--tw-text-opacity))}.text-gray-500{--tw-text-opacity:1;color:rgb(107 114 128/var(--tw-text-opacity))}.text-gray-900{--tw-text-opacity:1;color:rgb(17 24 39/var(--tw-text-opacity))}.text-gray-600{--tw-text-opacity:1;color:rgb(75 85 99/var(--tw-text-opacity))}.text-gray-700{--tw-text-opacity:1;color:rgb(55 65 81/var(--tw-text-opacity))}.text-gray-400{--tw-text-opacity:1;color:rgb(156 163 175/var(--tw-text-opacity))}.text-green-700{--tw-text-opacity:1;color:rgb(21 128 61/var(--tw-text-opacity))}.text-blue-700{--tw-text-opacity:1;color:rgb(29 78 216/var(--tw-text-opacity))}.text-yellow-800{--tw-text-opacity:1;color:rgb(133 77 14/var(--tw-text-opacity))}.text-red-700{--tw-text-opacity:1;color:rgb(185 28 28/var(--tw-text-opacity))}.text-purple-700{--tw-text-opacity:1;color:rgb(126 34 206/var(--tw-text-opacity))}.text-red-500{--tw-text-opacity:1;color:rgb(239 68 68/var(--tw-text-opacity))}.shadow-sm{--tw-shadow:0 1px 2px 0 #0000000d;--tw-shadow-colored:0 1px 2px 0 var(--tw-shadow-color)}.shadow-md,.shadow-sm{box-shadow:var(--tw-ring-offset-shadow,0 0 #0000),var(--tw-ring-shadow,0 0 #0000),var(--tw-shadow)}.shadow-md{--tw-shadow:0 4px 6px -1px #0000001a,0 2px 4px -2px #0000001a;--tw-shadow-colored:0 4px 6px -1px var(--tw-shadow-color),0 2px 4px -2px var(--tw-shadow-color)}.ring-1{--tw-ring-offset-shadow:var(--tw-ring-inset) 0 0 0 var(--tw-ring-offset-width) var(--tw-ring-offset-color);--tw-ring-shadow:var(--tw-ring-inset) 0 0 0 calc(1px + var(--tw-ring-offset-width)) var(--tw-ring-color);box-shadow:var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow,0 0 #0000)}.ring-inset{--tw-ring-inset:inset}.ring-green-600\/20{--tw-ring-color:#16a34a33}.ring-blue-700\/10{--tw-ring-color:#1d4ed81a}.ring-yellow-600\/20{--tw-ring-color:#ca8a0433}.ring-red-600\/10{--tw-ring-color:#dc26261a}.ring-gray-500\/10{--tw-ring-color:#6b72801a}.ring-gray-500{--tw-ring-opacity:1;--tw-ring-color:rgb(107 114 128/var(--tw-ring-opacity))}.ring-purple-700\/10{--tw-ring-color:#7e22ce1a}.transition{transition-duration:.15s;transition-property:color,background-color,border-color,fill,stroke,opacity,box-shadow,transform,filter,-webkit-text-decoration-color,-webkit-backdrop-filter;transition-property:color,background-color,border-color,text-decoration-color,fill,stroke,opacity,box-shadow,transform,filter,backdrop-filter;transition-property:color,background-color,border-color,text-decoration-color,fill,stroke,opacity,box-shadow,transform,filter,backdrop-filter,-webkit-text-decoration-color,-webkit-backdrop-filter;transition-timing-function:cubic-bezier(.4,0,.2,1)}.duration-500{transition-duration:.5s}.pagination{display:inline-flex;position:relative;z-index:0}.pagination>:not([hidden])~:not([hidden]){--tw-space-x-reverse:0;margin-left:calc(-1px*(1 - var(--tw-space-x-reverse)));margin-right:calc(-1px*var(--tw-space-x-reverse))}.pagination{--tw-shadow:0 1px 2px 0 #0000000d;--tw-shadow-colored:0 1px 2px 0 var(--tw-shadow-color);border-radius:.375rem;box-shadow:var(--tw-ring-offset-shadow,0 0 #0000),var(--tw-ring-shadow,0 0 #0000),var(--tw-shadow)}.pagination .prev a{--tw-border-opacity:1;--tw-bg-opacity:1;--tw-text-opacity:1;align-items:center;background-color:rgb(255 255 255/var(--tw-bg-opacity));border-bottom-left-radius:.375rem;border-color:rgb(209 213 219/var(--tw-border-opacity));border-top-left-radius:.375rem;border-width:1px;color:rgb(107 114 128/var(--tw-text-opacity));display:inline-flex;font-size:.875rem;font-weight:500;line-height:1.25rem;padding:.5rem;position:relative}.pagination .prev a:hover{--tw-bg-opacity:1;background-color:rgb(249 250 251/var(--tw-bg-opacity))}.pagination .next a{--tw-border-opacity:1;--tw-bg-opacity:1;--tw-text-opacity:1;align-items:center;background-color:rgb(255 255 255/var(--tw-bg-opacity));border-bottom-right-radius:.375rem;border-color:rgb(209 213 219/var(--tw-border-opacity));border-top-right-radius:.375rem;border-width:1px;color:rgb(107 114 128/var(--tw-text-opacity));display:inline-flex;font-size:.875rem;font-weight:500;line-height:1.25rem;padding:.5rem;position:relative}.pagination .next a:hover{--tw-bg-opacity:1;background-color:rgb(249 250 251/var(--tw-bg-opacity))}.pagination .current{--tw-text-opacity:1;border-color:rgb(239 68 68/var(--tw-border-opacity));border-top-width:2px;color:rgb(239 68 68/var(--tw-text-opacity));padding-left:1rem;padding-right:1rem;padding-top:1rem}.pagination .current,.pagination a{--tw-border-opacity:1;align-items:center;display:inline-flex;font-size:.875rem;font-weight:500;line-height:1.25rem}.pagination a{--tw-bg-opacity:1;--tw-text-opacity:1;background-color:rgb(255 255 255/var(--tw-bg-opacity));border-color:rgb(209 213 219/var(--tw-border-opacity));border-width:1px;color:rgb(107 114 128/var(--tw-text-opacity));padding:.5rem 1rem;position:relative}.pagination a:hover{--tw-bg-opacity:1;background-color:rgb(249 250 251/var(--tw-bg-opacity))}.pagination .disabled{--tw-border-opacity:1;--tw-bg-opacity:1;--tw-text-opacity:1;align-items:center;background-color:rgb(255 255 255/var(--tw-bg-opacity));border-color:rgb(209 213 219/var(--tw-border-opacity));border-width:1px;color:rgb(107 114 128/var(--tw-text-opacity));display:inline-flex;font-size:.875rem;font-weight:500;line-height:1.25rem;opacity:.4;padding:.5rem 1rem;position:relative}.pagination .disabled:hover{--tw-bg-opacity:1;background-color:rgb(249 250 251/var(--tw-bg-opacity))}.pagination .gap{background-color:rgb(255 255 255/var(--tw-bg-opacity));border-color:rgb(209 213 219/var(--tw-border-opacity));color:rgb(55 65 81/var(--tw-text-opacity))}.pagination .active,.pagination .gap{--tw-border-opacity:1;--tw-bg-opacity:1;--tw-text-opacity:1;align-items:center;border-width:1px;display:inline-flex;font-size:.875rem;font-weight:500;line-height:1.25rem;padding:.5rem 1rem;position:relative}.pagination .active{background-color:rgb(254 242 242/var(--tw-bg-opacity));border-color:rgb(239 68 68/var(--tw-border-opacity));color:rgb(239 68 68/var(--tw-text-opacity));z-index:10}.pagination .next_page,.pagination .previous_page{display:none}.hover\:bg-red-600:hover{--tw-bg-opacity:1;background-color:rgb(220 38 38/var(--tw-bg-opacity))}.hover\:bg-gray-300:hover{--tw-bg-opacity:1;background-color:rgb(209 213 219/var(--tw-bg-opacity))}.hover\:bg-gray-200:hover{--tw-bg-opacity:1;background-color:rgb(229 231 235/var(--tw-bg-opacity))}.hover\:text-gray-900:hover{--tw-text-opacity:1;color:rgb(17 24 39/var(--tw-text-opacity))}.focus\:border-red-500:focus{--tw-border-opacity:1;border-color:rgb(239 68 68/var(--tw-border-opacity))}.focus\:outline-none:focus{outline:2px solid #0000;outline-offset:2px}.focus\:ring-red-500:focus{--tw-ring-opacity:1;--tw-ring-color:rgb(239 68 68/var(--tw-ring-opacity))}@media (min-width:640px){.sm\:col-span-2{grid-column:span 2/span 2}.sm\:-mx-6{margin-left:-1.5rem;margin-right:-1.5rem}.sm\:mt-0{margin-top:0}.sm\:flex{display:flex}.sm\:grid{display:grid}.sm\:flex-auto{flex:1 1 auto}.sm\:grid-cols-3{grid-template-columns:repeat(3,minmax(0,1fr))}.sm\:items-center{align-items:center}.sm\:gap-4{gap:1rem}.sm\:px-6{padding-left:1.5rem;padding-right:1.5rem}.sm\:px-0{padding-right:0}.sm\:pl-0,.sm\:px-0{padding-left:0}.sm\:pr-0{padding-right:0}.sm\:text-sm{font-size:.875rem;line-height:1.25rem}}@media (min-width:1024px){.lg\:fixed{position:fixed}.lg\:inset-y-0{bottom:0;top:0}.lg\:z-50{z-index:50}.lg\:-mx-8{margin-left:-2rem;margin-right:-2rem}.lg\:flex{display:flex}.lg\:hidden{display:none}.lg\:w-72{width:18rem}.lg\:flex-col{flex-direction:column}.lg\:px-8{padding-left:2rem;padding-right:2rem}.lg\:pl-72{padding-left:18rem}}
1
+ /*! tailwindcss v3.1.3 | MIT License | https://tailwindcss.com*/*,:after,:before{border:0 solid #e5e7eb;box-sizing:border-box}:after,:before{--tw-content:""}html{-webkit-text-size-adjust:100%;font-family:Fira Sans,sans-serif,ui-sans-serif,system-ui,-apple-system,BlinkMacSystemFont,Segoe UI,Roboto,Helvetica Neue,Arial,Noto Sans,Apple Color Emoji,Segoe UI Emoji,Segoe UI Symbol,Noto Color Emoji;line-height:1.5;-moz-tab-size:4;-o-tab-size:4;tab-size:4}body{line-height:inherit;margin:0}hr{border-top-width:1px;color:inherit;height:0}abbr:where([title]){-webkit-text-decoration:underline dotted;text-decoration:underline dotted}h1,h2,h3,h4,h5,h6{font-size:inherit;font-weight:inherit}a{color:inherit;text-decoration:inherit}b,strong{font-weight:bolder}code,kbd,pre,samp{font-family:ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,Liberation Mono,Courier New,monospace;font-size:1em}small{font-size:80%}sub,sup{font-size:75%;line-height:0;position:relative;vertical-align:initial}sub{bottom:-.25em}sup{top:-.5em}table{border-collapse:collapse;border-color:inherit;text-indent:0}button,input,optgroup,select,textarea{color:inherit;font-family:inherit;font-size:100%;font-weight:inherit;line-height:inherit;margin:0;padding:0}button,select{text-transform:none}[type=button],[type=reset],[type=submit],button{-webkit-appearance:button;background-color:initial;background-image:none}:-moz-focusring{outline:auto}:-moz-ui-invalid{box-shadow:none}progress{vertical-align:initial}::-webkit-inner-spin-button,::-webkit-outer-spin-button{height:auto}[type=search]{-webkit-appearance:textfield;outline-offset:-2px}::-webkit-search-decoration{-webkit-appearance:none}::-webkit-file-upload-button{-webkit-appearance:button;font:inherit}summary{display:list-item}blockquote,dd,dl,figure,h1,h2,h3,h4,h5,h6,hr,p,pre{margin:0}fieldset{margin:0}fieldset,legend{padding:0}menu,ol,ul{list-style:none;margin:0;padding:0}textarea{resize:vertical}input::-moz-placeholder,textarea::-moz-placeholder{color:#9ca3af;opacity:1}input:-ms-input-placeholder,textarea:-ms-input-placeholder{color:#9ca3af;opacity:1}input::placeholder,textarea::placeholder{color:#9ca3af;opacity:1}[role=button],button{cursor:pointer}:disabled{cursor:default}audio,canvas,embed,iframe,img,object,svg,video{display:block;vertical-align:middle}img,video{height:auto;max-width:100%}[multiple],[type=date],[type=datetime-local],[type=email],[type=month],[type=number],[type=password],[type=search],[type=tel],[type=text],[type=time],[type=url],[type=week],select,textarea{--tw-shadow:0 0 #0000;-webkit-appearance:none;-moz-appearance:none;appearance:none;background-color:#fff;border-color:#6b7280;border-radius:0;border-width:1px;font-size:1rem;line-height:1.5rem;padding:.5rem .75rem}[multiple]:focus,[type=date]:focus,[type=datetime-local]:focus,[type=email]:focus,[type=month]:focus,[type=number]:focus,[type=password]:focus,[type=search]:focus,[type=tel]:focus,[type=text]:focus,[type=time]:focus,[type=url]:focus,[type=week]:focus,select:focus,textarea:focus{--tw-ring-inset:var(--tw-empty,/*!*/ /*!*/);--tw-ring-offset-width:0px;--tw-ring-offset-color:#fff;--tw-ring-color:#2563eb;--tw-ring-offset-shadow:var(--tw-ring-inset) 0 0 0 var(--tw-ring-offset-width) var(--tw-ring-offset-color);--tw-ring-shadow:var(--tw-ring-inset) 0 0 0 calc(1px + var(--tw-ring-offset-width)) var(--tw-ring-color);border-color:#2563eb;box-shadow:var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow);outline:2px solid #0000;outline-offset:2px}input::-moz-placeholder,textarea::-moz-placeholder{color:#6b7280;opacity:1}input:-ms-input-placeholder,textarea:-ms-input-placeholder{color:#6b7280;opacity:1}input::placeholder,textarea::placeholder{color:#6b7280;opacity:1}::-webkit-datetime-edit-fields-wrapper{padding:0}::-webkit-date-and-time-value{min-height:1.5em}select{color-adjust:exact;background-image:url("data:image/svg+xml;charset=utf-8,%3Csvg xmlns='http://www.w3.org/2000/svg' fill='none' viewBox='0 0 20 20'%3E%3Cpath stroke='%236b7280' stroke-linecap='round' stroke-linejoin='round' stroke-width='1.5' d='m6 8 4 4 4-4'/%3E%3C/svg%3E");background-position:right .5rem center;background-repeat:no-repeat;background-size:1.5em 1.5em;padding-right:2.5rem;-webkit-print-color-adjust:exact}[multiple]{color-adjust:unset;background-image:none;background-position:0 0;background-repeat:unset;background-size:initial;padding-right:.75rem;-webkit-print-color-adjust:unset}[type=checkbox],[type=radio]{color-adjust:exact;--tw-shadow:0 0 #0000;-webkit-appearance:none;-moz-appearance:none;appearance:none;background-color:#fff;background-origin:border-box;border-color:#6b7280;border-width:1px;color:#2563eb;display:inline-block;flex-shrink:0;height:1rem;padding:0;-webkit-print-color-adjust:exact;-webkit-user-select:none;-moz-user-select:none;-ms-user-select:none;user-select:none;vertical-align:middle;width:1rem}[type=checkbox]{border-radius:0}[type=radio]{border-radius:100%}[type=checkbox]:focus,[type=radio]:focus{--tw-ring-inset:var(--tw-empty,/*!*/ /*!*/);--tw-ring-offset-width:2px;--tw-ring-offset-color:#fff;--tw-ring-color:#2563eb;--tw-ring-offset-shadow:var(--tw-ring-inset) 0 0 0 var(--tw-ring-offset-width) var(--tw-ring-offset-color);--tw-ring-shadow:var(--tw-ring-inset) 0 0 0 calc(2px + var(--tw-ring-offset-width)) var(--tw-ring-color);box-shadow:var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow);outline:2px solid #0000;outline-offset:2px}[type=checkbox]:checked,[type=radio]:checked{background-color:currentColor;background-position:50%;background-repeat:no-repeat;background-size:100% 100%;border-color:#0000}[type=checkbox]:checked{background-image:url("data:image/svg+xml;charset=utf-8,%3Csvg viewBox='0 0 16 16' fill='%23fff' xmlns='http://www.w3.org/2000/svg'%3E%3Cpath d='M12.207 4.793a1 1 0 0 1 0 1.414l-5 5a1 1 0 0 1-1.414 0l-2-2a1 1 0 0 1 1.414-1.414L6.5 9.086l4.293-4.293a1 1 0 0 1 1.414 0z'/%3E%3C/svg%3E")}[type=radio]:checked{background-image:url("data:image/svg+xml;charset=utf-8,%3Csvg viewBox='0 0 16 16' fill='%23fff' xmlns='http://www.w3.org/2000/svg'%3E%3Ccircle cx='8' cy='8' r='3'/%3E%3C/svg%3E")}[type=checkbox]:checked:focus,[type=checkbox]:checked:hover,[type=checkbox]:indeterminate,[type=radio]:checked:focus,[type=radio]:checked:hover{background-color:currentColor;border-color:#0000}[type=checkbox]:indeterminate{background-image:url("data:image/svg+xml;charset=utf-8,%3Csvg xmlns='http://www.w3.org/2000/svg' fill='none' viewBox='0 0 16 16'%3E%3Cpath stroke='%23fff' stroke-linecap='round' stroke-linejoin='round' stroke-width='2' d='M4 8h8'/%3E%3C/svg%3E");background-position:50%;background-repeat:no-repeat;background-size:100% 100%}[type=checkbox]:indeterminate:focus,[type=checkbox]:indeterminate:hover{background-color:currentColor;border-color:#0000}[type=file]{background:unset;border-color:inherit;border-radius:0;border-width:0;font-size:unset;line-height:inherit;padding:0}[type=file]:focus{outline:1px auto -webkit-focus-ring-color}*,:after,:before{--tw-border-spacing-x:0;--tw-border-spacing-y:0;--tw-translate-x:0;--tw-translate-y:0;--tw-rotate:0;--tw-skew-x:0;--tw-skew-y:0;--tw-scale-x:1;--tw-scale-y:1;--tw-pan-x: ;--tw-pan-y: ;--tw-pinch-zoom: ;--tw-scroll-snap-strictness:proximity;--tw-ordinal: ;--tw-slashed-zero: ;--tw-numeric-figure: ;--tw-numeric-spacing: ;--tw-numeric-fraction: ;--tw-ring-inset: ;--tw-ring-offset-width:0px;--tw-ring-offset-color:#fff;--tw-ring-color:#3b82f680;--tw-ring-offset-shadow:0 0 #0000;--tw-ring-shadow:0 0 #0000;--tw-shadow:0 0 #0000;--tw-shadow-colored:0 0 #0000;--tw-blur: ;--tw-brightness: ;--tw-contrast: ;--tw-grayscale: ;--tw-hue-rotate: ;--tw-invert: ;--tw-saturate: ;--tw-sepia: ;--tw-drop-shadow: ;--tw-backdrop-blur: ;--tw-backdrop-brightness: ;--tw-backdrop-contrast: ;--tw-backdrop-grayscale: ;--tw-backdrop-hue-rotate: ;--tw-backdrop-invert: ;--tw-backdrop-opacity: ;--tw-backdrop-saturate: ;--tw-backdrop-sepia: }::-webkit-backdrop{--tw-border-spacing-x:0;--tw-border-spacing-y:0;--tw-translate-x:0;--tw-translate-y:0;--tw-rotate:0;--tw-skew-x:0;--tw-skew-y:0;--tw-scale-x:1;--tw-scale-y:1;--tw-pan-x: ;--tw-pan-y: ;--tw-pinch-zoom: ;--tw-scroll-snap-strictness:proximity;--tw-ordinal: ;--tw-slashed-zero: ;--tw-numeric-figure: ;--tw-numeric-spacing: ;--tw-numeric-fraction: ;--tw-ring-inset: ;--tw-ring-offset-width:0px;--tw-ring-offset-color:#fff;--tw-ring-color:#3b82f680;--tw-ring-offset-shadow:0 0 #0000;--tw-ring-shadow:0 0 #0000;--tw-shadow:0 0 #0000;--tw-shadow-colored:0 0 #0000;--tw-blur: ;--tw-brightness: ;--tw-contrast: ;--tw-grayscale: ;--tw-hue-rotate: ;--tw-invert: ;--tw-saturate: ;--tw-sepia: ;--tw-drop-shadow: ;--tw-backdrop-blur: ;--tw-backdrop-brightness: ;--tw-backdrop-contrast: ;--tw-backdrop-grayscale: ;--tw-backdrop-hue-rotate: ;--tw-backdrop-invert: ;--tw-backdrop-opacity: ;--tw-backdrop-saturate: ;--tw-backdrop-sepia: }::backdrop{--tw-border-spacing-x:0;--tw-border-spacing-y:0;--tw-translate-x:0;--tw-translate-y:0;--tw-rotate:0;--tw-skew-x:0;--tw-skew-y:0;--tw-scale-x:1;--tw-scale-y:1;--tw-pan-x: ;--tw-pan-y: ;--tw-pinch-zoom: ;--tw-scroll-snap-strictness:proximity;--tw-ordinal: ;--tw-slashed-zero: ;--tw-numeric-figure: ;--tw-numeric-spacing: ;--tw-numeric-fraction: ;--tw-ring-inset: ;--tw-ring-offset-width:0px;--tw-ring-offset-color:#fff;--tw-ring-color:#3b82f680;--tw-ring-offset-shadow:0 0 #0000;--tw-ring-shadow:0 0 #0000;--tw-shadow:0 0 #0000;--tw-shadow-colored:0 0 #0000;--tw-blur: ;--tw-brightness: ;--tw-contrast: ;--tw-grayscale: ;--tw-hue-rotate: ;--tw-invert: ;--tw-saturate: ;--tw-sepia: ;--tw-drop-shadow: ;--tw-backdrop-blur: ;--tw-backdrop-brightness: ;--tw-backdrop-contrast: ;--tw-backdrop-grayscale: ;--tw-backdrop-hue-rotate: ;--tw-backdrop-invert: ;--tw-backdrop-opacity: ;--tw-backdrop-saturate: ;--tw-backdrop-sepia: }.sr-only{clip:rect(0,0,0,0);border-width:0;height:1px;margin:-1px;overflow:hidden;padding:0;position:absolute;white-space:nowrap;width:1px}.static{position:static}.fixed{position:fixed}.absolute{position:absolute}.relative{position:relative}.sticky{position:-webkit-sticky;position:sticky}.inset-0{bottom:0;left:0;right:0;top:0}.left-full{left:100%}.top-0{top:0}.z-50{z-index:50}.z-40{z-index:40}.col-span-1{grid-column:span 1/span 1}.col-span-11{grid-column:span 11/span 11}.-m-2\.5{margin:-.625rem}.-m-2{margin:-.5rem}.-mx-4{margin-left:-1rem;margin-right:-1rem}.-my-2{margin-bottom:-.5rem;margin-top:-.5rem}.-mx-2{margin-left:-.5rem;margin-right:-.5rem}.mx-auto{margin-left:auto;margin-right:auto}.mt-4{margin-top:1rem}.mt-2{margin-top:.5rem}.mt-8{margin-top:2rem}.mt-6{margin-top:1.5rem}.mt-1{margin-top:.25rem}.mr-16{margin-right:4rem}.ml-2{margin-left:.5rem}.block{display:block}.inline-block{display:inline-block}.flex{display:flex}.inline-flex{display:inline-flex}.table{display:table}.flow-root{display:flow-root}.grid{display:grid}.hidden{display:none}.h-6{height:1.5rem}.h-16{height:4rem}.h-7{height:1.75rem}.h-12{height:3rem}.h-5{height:1.25rem}.w-full{width:100%}.w-16{width:4rem}.w-6{width:1.5rem}.w-auto{width:auto}.w-5{width:1.25rem}.min-w-full{min-width:100%}.max-w-xs{max-width:20rem}.flex-1{flex:1 1 0%}.shrink-0{flex-shrink:0}.grow{flex-grow:1}.grid-cols-12{grid-template-columns:repeat(12,minmax(0,1fr))}.flex-col{flex-direction:column}.items-end{align-items:flex-end}.items-center{align-items:center}.justify-end{justify-content:flex-end}.justify-center{justify-content:center}.justify-between{justify-content:space-between}.gap-x-1{-moz-column-gap:.25rem;column-gap:.25rem}.gap-y-5{row-gap:1.25rem}.gap-y-7{row-gap:1.75rem}.gap-x-3{-moz-column-gap:.75rem;column-gap:.75rem}.gap-x-2{-moz-column-gap:.5rem;column-gap:.5rem}.gap-x-6{-moz-column-gap:1.5rem;column-gap:1.5rem}.space-y-1>:not([hidden])~:not([hidden]){--tw-space-y-reverse:0;margin-bottom:calc(.25rem*var(--tw-space-y-reverse));margin-top:calc(.25rem*(1 - var(--tw-space-y-reverse)))}.divide-y>:not([hidden])~:not([hidden]){--tw-divide-y-reverse:0;border-bottom-width:calc(1px*var(--tw-divide-y-reverse));border-top-width:calc(1px*(1 - var(--tw-divide-y-reverse)))}.divide-gray-300>:not([hidden])~:not([hidden]){--tw-divide-opacity:1;border-color:rgb(209 213 219/var(--tw-divide-opacity))}.divide-gray-200>:not([hidden])~:not([hidden]){--tw-divide-opacity:1;border-color:rgb(229 231 235/var(--tw-divide-opacity))}.divide-gray-100>:not([hidden])~:not([hidden]){--tw-divide-opacity:1;border-color:rgb(243 244 246/var(--tw-divide-opacity))}.overflow-x-auto{overflow-x:auto}.overflow-y-auto{overflow-y:auto}.truncate{overflow:hidden;text-overflow:ellipsis}.truncate,.whitespace-nowrap{white-space:nowrap}.rounded-md{border-radius:.375rem}.border{border-width:1px}.border-t{border-top-width:1px}.border-b{border-bottom-width:1px}.border-gray-100{--tw-border-opacity:1;border-color:rgb(243 244 246/var(--tw-border-opacity))}.border-gray-300{--tw-border-opacity:1;border-color:rgb(209 213 219/var(--tw-border-opacity))}.bg-red-500{--tw-bg-opacity:1;background-color:rgb(239 68 68/var(--tw-bg-opacity))}.bg-gray-200{--tw-bg-opacity:1;background-color:rgb(229 231 235/var(--tw-bg-opacity))}.bg-white{--tw-bg-opacity:1;background-color:rgb(255 255 255/var(--tw-bg-opacity))}.bg-black{--tw-bg-opacity:1;background-color:rgb(0 0 0/var(--tw-bg-opacity))}.bg-green-50{--tw-bg-opacity:1;background-color:rgb(240 253 244/var(--tw-bg-opacity))}.bg-blue-50{--tw-bg-opacity:1;background-color:rgb(239 246 255/var(--tw-bg-opacity))}.bg-yellow-50{--tw-bg-opacity:1;background-color:rgb(254 252 232/var(--tw-bg-opacity))}.bg-red-50{--tw-bg-opacity:1;background-color:rgb(254 242 242/var(--tw-bg-opacity))}.bg-gray-50{--tw-bg-opacity:1;background-color:rgb(249 250 251/var(--tw-bg-opacity))}.bg-purple-50{--tw-bg-opacity:1;background-color:rgb(250 245 255/var(--tw-bg-opacity))}.bg-gray-900\/80{background-color:#111827cc}.bg-opacity-30{--tw-bg-opacity:0.3}.p-2{padding:.5rem}.\!p-0{padding:0!important}.p-2\.5{padding:.625rem}.p-1{padding:.25rem}.py-2{padding-bottom:.5rem;padding-top:.5rem}.py-3{padding-bottom:.75rem;padding-top:.75rem}.px-3{padding-left:.75rem;padding-right:.75rem}.py-4{padding-bottom:1rem;padding-top:1rem}.px-4{padding-left:1rem;padding-right:1rem}.py-5{padding-bottom:1.25rem;padding-top:1.25rem}.py-6{padding-bottom:1.5rem;padding-top:1.5rem}.px-2{padding-left:.5rem;padding-right:.5rem}.px-0{padding-left:0;padding-right:0}.py-1{padding-bottom:.25rem;padding-top:.25rem}.px-6{padding-left:1.5rem;padding-right:1.5rem}.py-10{padding-bottom:2.5rem;padding-top:2.5rem}.px-5{padding-left:1.25rem;padding-right:1.25rem}.pl-4{padding-left:1rem}.pr-3{padding-right:.75rem}.pl-3{padding-left:.75rem}.pr-4{padding-right:1rem}.pt-2{padding-top:.5rem}.pb-4{padding-bottom:1rem}.pb-3{padding-bottom:.75rem}.pt-5{padding-top:1.25rem}.pb-2{padding-bottom:.5rem}.text-left{text-align:left}.text-right{text-align:right}.align-middle{vertical-align:middle}.text-sm{font-size:.875rem;line-height:1.25rem}.text-base{font-size:1rem;line-height:1.5rem}.text-xl{font-size:1.25rem;line-height:1.75rem}.text-2xl{font-size:1.5rem;line-height:2rem}.font-medium{font-weight:500}.font-normal{font-weight:400}.font-semibold{font-weight:600}.uppercase{text-transform:uppercase}.leading-6{line-height:1.5rem}.leading-7{line-height:1.75rem}.tracking-wide{letter-spacing:.025em}.text-white{--tw-text-opacity:1;color:rgb(255 255 255/var(--tw-text-opacity))}.text-gray-500{--tw-text-opacity:1;color:rgb(107 114 128/var(--tw-text-opacity))}.text-gray-900{--tw-text-opacity:1;color:rgb(17 24 39/var(--tw-text-opacity))}.text-gray-600{--tw-text-opacity:1;color:rgb(75 85 99/var(--tw-text-opacity))}.text-gray-700{--tw-text-opacity:1;color:rgb(55 65 81/var(--tw-text-opacity))}.text-gray-300{--tw-text-opacity:1;color:rgb(209 213 219/var(--tw-text-opacity))}.text-gray-400{--tw-text-opacity:1;color:rgb(156 163 175/var(--tw-text-opacity))}.text-green-700{--tw-text-opacity:1;color:rgb(21 128 61/var(--tw-text-opacity))}.text-blue-700{--tw-text-opacity:1;color:rgb(29 78 216/var(--tw-text-opacity))}.text-yellow-800{--tw-text-opacity:1;color:rgb(133 77 14/var(--tw-text-opacity))}.text-red-700{--tw-text-opacity:1;color:rgb(185 28 28/var(--tw-text-opacity))}.text-purple-700{--tw-text-opacity:1;color:rgb(126 34 206/var(--tw-text-opacity))}.text-red-500{--tw-text-opacity:1;color:rgb(239 68 68/var(--tw-text-opacity))}.underline{-webkit-text-decoration-line:underline;text-decoration-line:underline}.shadow-md{--tw-shadow:0 4px 6px -1px #0000001a,0 2px 4px -2px #0000001a;--tw-shadow-colored:0 4px 6px -1px var(--tw-shadow-color),0 2px 4px -2px var(--tw-shadow-color)}.shadow-md,.shadow-sm{box-shadow:var(--tw-ring-offset-shadow,0 0 #0000),var(--tw-ring-shadow,0 0 #0000),var(--tw-shadow)}.shadow-sm{--tw-shadow:0 1px 2px 0 #0000000d;--tw-shadow-colored:0 1px 2px 0 var(--tw-shadow-color)}.ring-1{--tw-ring-offset-shadow:var(--tw-ring-inset) 0 0 0 var(--tw-ring-offset-width) var(--tw-ring-offset-color);--tw-ring-shadow:var(--tw-ring-inset) 0 0 0 calc(1px + var(--tw-ring-offset-width)) var(--tw-ring-color);box-shadow:var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow,0 0 #0000)}.ring-inset{--tw-ring-inset:inset}.ring-green-600\/20{--tw-ring-color:#16a34a33}.ring-blue-700\/10{--tw-ring-color:#1d4ed81a}.ring-yellow-600\/20{--tw-ring-color:#ca8a0433}.ring-red-600\/10{--tw-ring-color:#dc26261a}.ring-gray-500\/10{--tw-ring-color:#6b72801a}.ring-gray-500{--tw-ring-opacity:1;--tw-ring-color:rgb(107 114 128/var(--tw-ring-opacity))}.ring-purple-700\/10{--tw-ring-color:#7e22ce1a}.transition{transition-duration:.15s;transition-property:color,background-color,border-color,fill,stroke,opacity,box-shadow,transform,filter,-webkit-text-decoration-color,-webkit-backdrop-filter;transition-property:color,background-color,border-color,text-decoration-color,fill,stroke,opacity,box-shadow,transform,filter,backdrop-filter;transition-property:color,background-color,border-color,text-decoration-color,fill,stroke,opacity,box-shadow,transform,filter,backdrop-filter,-webkit-text-decoration-color,-webkit-backdrop-filter;transition-timing-function:cubic-bezier(.4,0,.2,1)}.duration-500{transition-duration:.5s}.pagination{display:inline-flex;position:relative;z-index:0}.pagination>:not([hidden])~:not([hidden]){--tw-space-x-reverse:0;margin-left:calc(-1px*(1 - var(--tw-space-x-reverse)));margin-right:calc(-1px*var(--tw-space-x-reverse))}.pagination{--tw-shadow:0 1px 2px 0 #0000000d;--tw-shadow-colored:0 1px 2px 0 var(--tw-shadow-color);border-radius:.375rem;box-shadow:var(--tw-ring-offset-shadow,0 0 #0000),var(--tw-ring-shadow,0 0 #0000),var(--tw-shadow)}.pagination .prev a{--tw-border-opacity:1;--tw-bg-opacity:1;--tw-text-opacity:1;align-items:center;background-color:rgb(255 255 255/var(--tw-bg-opacity));border-bottom-left-radius:.375rem;border-color:rgb(209 213 219/var(--tw-border-opacity));border-top-left-radius:.375rem;border-width:1px;color:rgb(107 114 128/var(--tw-text-opacity));display:inline-flex;font-size:.875rem;font-weight:500;line-height:1.25rem;padding:.5rem;position:relative}.pagination .prev a:hover{--tw-bg-opacity:1;background-color:rgb(249 250 251/var(--tw-bg-opacity))}.pagination .next a{--tw-border-opacity:1;--tw-bg-opacity:1;--tw-text-opacity:1;align-items:center;background-color:rgb(255 255 255/var(--tw-bg-opacity));border-bottom-right-radius:.375rem;border-color:rgb(209 213 219/var(--tw-border-opacity));border-top-right-radius:.375rem;border-width:1px;color:rgb(107 114 128/var(--tw-text-opacity));display:inline-flex;font-size:.875rem;font-weight:500;line-height:1.25rem;padding:.5rem;position:relative}.pagination .next a:hover{--tw-bg-opacity:1;background-color:rgb(249 250 251/var(--tw-bg-opacity))}.pagination .current{--tw-text-opacity:1;border-color:rgb(239 68 68/var(--tw-border-opacity));border-top-width:2px;color:rgb(239 68 68/var(--tw-text-opacity));padding-left:1rem;padding-right:1rem;padding-top:1rem}.pagination .current,.pagination a{--tw-border-opacity:1;align-items:center;display:inline-flex;font-size:.875rem;font-weight:500;line-height:1.25rem}.pagination a{--tw-bg-opacity:1;--tw-text-opacity:1;background-color:rgb(255 255 255/var(--tw-bg-opacity));border-color:rgb(209 213 219/var(--tw-border-opacity));border-width:1px;color:rgb(107 114 128/var(--tw-text-opacity));padding:.5rem 1rem;position:relative}.pagination a:hover{--tw-bg-opacity:1;background-color:rgb(249 250 251/var(--tw-bg-opacity))}.pagination .disabled{--tw-border-opacity:1;--tw-bg-opacity:1;--tw-text-opacity:1;align-items:center;background-color:rgb(255 255 255/var(--tw-bg-opacity));border-color:rgb(209 213 219/var(--tw-border-opacity));border-width:1px;color:rgb(107 114 128/var(--tw-text-opacity));display:inline-flex;font-size:.875rem;font-weight:500;line-height:1.25rem;opacity:.4;padding:.5rem 1rem;position:relative}.pagination .disabled:hover{--tw-bg-opacity:1;background-color:rgb(249 250 251/var(--tw-bg-opacity))}.pagination .gap{background-color:rgb(255 255 255/var(--tw-bg-opacity));border-color:rgb(209 213 219/var(--tw-border-opacity));color:rgb(55 65 81/var(--tw-text-opacity))}.pagination .active,.pagination .gap{--tw-border-opacity:1;--tw-bg-opacity:1;--tw-text-opacity:1;align-items:center;border-width:1px;display:inline-flex;font-size:.875rem;font-weight:500;line-height:1.25rem;padding:.5rem 1rem;position:relative}.pagination .active{background-color:rgb(254 242 242/var(--tw-bg-opacity));border-color:rgb(239 68 68/var(--tw-border-opacity));color:rgb(239 68 68/var(--tw-text-opacity));z-index:10}.pagination .next_page,.pagination .previous_page{display:none}#result pre{background-color:rgb(0 0 0/var(--tw-bg-opacity));color:rgb(255 255 255/var(--tw-text-opacity));margin-bottom:.5rem;margin-top:.5rem;padding:.75rem}#result p code,#result pre{--tw-bg-opacity:1;--tw-text-opacity:1;border-radius:.375rem}#result p code{background-color:rgb(229 231 235/var(--tw-bg-opacity));color:rgb(75 85 99/var(--tw-text-opacity));font-size:.875rem;line-height:1.25rem;padding:.125rem .5rem}.hover\:bg-red-600:hover{--tw-bg-opacity:1;background-color:rgb(220 38 38/var(--tw-bg-opacity))}.hover\:bg-gray-300:hover{--tw-bg-opacity:1;background-color:rgb(209 213 219/var(--tw-bg-opacity))}.hover\:bg-gray-100:hover{--tw-bg-opacity:1;background-color:rgb(243 244 246/var(--tw-bg-opacity))}.hover\:bg-gray-200:hover{--tw-bg-opacity:1;background-color:rgb(229 231 235/var(--tw-bg-opacity))}.hover\:bg-red-400:hover{--tw-bg-opacity:1;background-color:rgb(248 113 113/var(--tw-bg-opacity))}.hover\:text-gray-900:hover{--tw-text-opacity:1;color:rgb(17 24 39/var(--tw-text-opacity))}.hover\:shadow-md:hover{--tw-shadow:0 4px 6px -1px #0000001a,0 2px 4px -2px #0000001a;--tw-shadow-colored:0 4px 6px -1px var(--tw-shadow-color),0 2px 4px -2px var(--tw-shadow-color);box-shadow:var(--tw-ring-offset-shadow,0 0 #0000),var(--tw-ring-shadow,0 0 #0000),var(--tw-shadow)}.focus\:border-red-500:focus{--tw-border-opacity:1;border-color:rgb(239 68 68/var(--tw-border-opacity))}.focus\:outline-none:focus{outline:2px solid #0000;outline-offset:2px}.focus\:ring-red-500:focus{--tw-ring-opacity:1;--tw-ring-color:rgb(239 68 68/var(--tw-ring-opacity))}@media (min-width:640px){.sm\:col-span-2{grid-column:span 2/span 2}.sm\:-mx-6{margin-left:-1.5rem;margin-right:-1.5rem}.sm\:mt-0{margin-top:0}.sm\:flex{display:flex}.sm\:grid{display:grid}.sm\:flex-auto{flex:1 1 auto}.sm\:grid-cols-3{grid-template-columns:repeat(3,minmax(0,1fr))}.sm\:items-center{align-items:center}.sm\:gap-4{gap:1rem}.sm\:px-6{padding-left:1.5rem;padding-right:1.5rem}.sm\:px-0{padding-right:0}.sm\:pl-0,.sm\:px-0{padding-left:0}.sm\:pr-0{padding-right:0}.sm\:text-sm{font-size:.875rem;line-height:1.25rem}}@media (min-width:1024px){.lg\:fixed{position:fixed}.lg\:inset-y-0{bottom:0;top:0}.lg\:z-50{z-index:50}.lg\:-mx-8{margin-left:-2rem;margin-right:-2rem}.lg\:flex{display:flex}.lg\:hidden{display:none}.lg\:w-72{width:18rem}.lg\:flex-col{flex-direction:column}.lg\:px-8{padding-left:2rem;padding-right:2rem}.lg\:pl-72{padding-left:18rem}}
@@ -0,0 +1,27 @@
1
+ import { Controller } from "@hotwired/stimulus"
2
+ import showdown from "showdown"
3
+ export default class extends Controller {
4
+ static targets = ["result", "loader", "btn"]
5
+ static values = { url: String }
6
+
7
+ assist() {
8
+ this.btnTarget.classList.add("hidden")
9
+ this.resultTarget.innerHTML = ""
10
+ this.loaderTarget.classList.remove("hidden")
11
+
12
+ fetch(this.urlValue)
13
+ .then(response => response.json())
14
+ .then(json => {
15
+ let result = json.choices[0].message.content
16
+ let converter = new showdown.Converter()
17
+ this.resultTarget.innerHTML = converter.makeHtml(result)
18
+
19
+ })
20
+ .catch(error => {
21
+ this.resultTarget.innerHTML = error
22
+ })
23
+ .finally(() => {
24
+ this.loaderTarget.classList.add("hidden")
25
+ })
26
+ }
27
+ }
@@ -43,7 +43,9 @@ export default class extends Controller {
43
43
  }
44
44
 
45
45
  _fetch() {
46
- this.frameTarget.reload()
46
+ if (this.hasFrameTarget) {
47
+ this.frameTarget.reload()
48
+ }
47
49
  }
48
50
 
49
51
  get enabled() {
@@ -39,4 +39,12 @@
39
39
 
40
40
  .pagination .previous_page, .pagination .next_page {
41
41
  @apply hidden;
42
+ }
43
+
44
+ #result pre {
45
+ @apply bg-black text-white p-3 rounded-md my-2;
46
+ }
47
+
48
+ #result p code {
49
+ @apply bg-gray-200 text-gray-600 px-2 py-0.5 text-sm rounded-md;
42
50
  }
@@ -13,4 +13,3 @@
13
13
  *= require_tree .
14
14
  *= require_self
15
15
  */
16
-
@@ -0,0 +1,30 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Eyeloupe
4
+ class AiAssistantResponsesController < ApplicationController
5
+
6
+ before_action :set_exception, only: %i[ show ]
7
+
8
+ def show
9
+ client = OpenAI::Client.new
10
+
11
+ code = File.read(@exception.file)
12
+
13
+ @response = client.chat(
14
+ parameters: {
15
+ model: Eyeloupe::configuration.openai_model,
16
+ messages: [{"role": "system", "content": "You are a Ruby on Rails software developer, you develop software programs and applications using programming languages like Ruby and Ruby on Rails and development tools."},
17
+ {"role": "user", "content": "I have a problem with my Ruby on Rails application. I am getting an error message that says: #{@exception.kind} #{@exception.message}. Here is my code, the error is in line #{@exception.line}: #{code}. Answer as concise as possible. Show me resulting code. The response should in Markdown format."}],
18
+ temperature: 0.7
19
+ })
20
+
21
+ render json: @response
22
+ end
23
+
24
+ private
25
+
26
+ def set_exception
27
+ @exception = Exception.find(params[:id])
28
+ end
29
+ end
30
+ end
@@ -7,7 +7,9 @@ module Eyeloupe
7
7
  # Delete all data in the database
8
8
  # DELETE /data
9
9
  def destroy
10
+ Exception.destroy_all
10
11
  InRequest.destroy_all
12
+ OutRequest.destroy_all
11
13
  redirect_to root_path, status: 303
12
14
  end
13
15
  end
@@ -0,0 +1,29 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Eyeloupe
4
+ class ExceptionsController < ApplicationController
5
+
6
+ before_action :set_exception, only: %i[ show ]
7
+
8
+ # GET /out_requests
9
+ def index
10
+ @pagy, @exceptions = pagy(Exception.all.order(created_at: :desc), items: 50)
11
+
12
+ render partial: 'frame' if params[:frame].present?
13
+ end
14
+
15
+ # GET /out_requests/1
16
+ def show
17
+ start = @exception.line - 5
18
+ start = 0 if start < 0
19
+ @line_numbers = [*start..@exception.line+6]
20
+ end
21
+
22
+ private
23
+ # Use callbacks to share common setup or constraints between actions.
24
+ def set_exception
25
+ @exception = Exception.find(params[:id])
26
+ end
27
+
28
+ end
29
+ end
@@ -0,0 +1,6 @@
1
+ module Eyeloupe
2
+ class Exception < ApplicationRecord
3
+ has_one :in_request, class_name: "Eyeloupe::InRequest"
4
+ has_one :out_request, class_name: "Eyeloupe::OutRequest"
5
+ end
6
+ end
@@ -1,4 +1,5 @@
1
1
  module Eyeloupe
2
2
  class InRequest < ApplicationRecord
3
+ has_one :exception, class_name: "Eyeloupe::Exception"
3
4
  end
4
5
  end
@@ -1,4 +1,5 @@
1
1
  module Eyeloupe
2
2
  class OutRequest < ApplicationRecord
3
+ has_one :exception, class_name: "Eyeloupe::Exception"
3
4
  end
4
5
  end
@@ -0,0 +1,39 @@
1
+ <%= turbo_frame_tag "frame" do %>
2
+ <div class="-mx-4 -my-2 overflow-x-auto sm:-mx-6 lg:-mx-8">
3
+ <div class="inline-block min-w-full py-2 align-middle sm:px-6 lg:px-8">
4
+
5
+ <table class="min-w-full divide-y divide-gray-300">
6
+ <thead>
7
+ <tr>
8
+ <th scope="col" class="py-3 pl-4 pr-3 text-left text-sm font-medium uppercase tracking-wide text-gray-500 sm:pl-0">Kind</th>
9
+ <th scope="col" class="px-3 py-3 text-left text-sm font-medium uppercase tracking-wide text-gray-500">Count</th>
10
+ <th scope="col" class="px-3 py-3 text-left text-sm font-medium uppercase tracking-wide text-gray-500">Occured</th>
11
+ <th scope="col" class="relative py-3 pl-3 pr-4 sm:pr-0">
12
+ <span class="sr-only">Details</span>
13
+ </th>
14
+ </tr>
15
+ </thead>
16
+ <tbody class="divide-y divide-gray-200">
17
+ <% @exceptions.each do |ex| %>
18
+ <tr>
19
+ <td class="whitespace-nowrap py-4 pl-4 pr-3 text-base font-medium text-gray-900 sm:pl-0">
20
+ <%= ex.kind %>
21
+ <p class="text-sm font-normal text-gray-500"><%= ex.message.truncate(100) %></p>
22
+ </td>
23
+ <td class="whitespace-nowrap px-3 py-4 text-base text-gray-500"><%= ex.count %></td>
24
+ <td class="whitespace-nowrap px-3 py-4 text-base text-gray-500"><%= distance_of_time_in_words(ex.updated_at, DateTime.now) %></td>
25
+ <td class="relative whitespace-nowrap py-4 pl-3 pr-4 text-right text-base font-medium sm:pr-0">
26
+ <%= link_to "Details", exception_path(ex), class: "text-gray-600 hover:text-gray-900", data: {"turbo_frame": "_top"} %>
27
+ </td>
28
+ </tr>
29
+ <% end %>
30
+ </tbody>
31
+ </table>
32
+ </div>
33
+ <aside class="mt-4 px-4 py-3 flex items-center justify-center sm:px-6" aria-label="Pagination">
34
+ <div class="flex-1 flex justify-center">
35
+ <%== pagy_nav(@pagy) %>
36
+ </div>
37
+ </aside>
38
+ </div>
39
+ <% end %>
@@ -0,0 +1,13 @@
1
+ <div class="px-4 sm:px-6 lg:px-8 bg-white rounded-md shadow-md py-5">
2
+ <div class="sm:flex sm:items-center">
3
+ <div class="sm:flex-auto">
4
+ <h1 class="text-xl font-semibold leading-6 text-gray-900">Exceptions</h1>
5
+ <p class="mt-2 text-sm text-gray-700">All exceptions that have been raised in the application.</p>
6
+ </div>
7
+ <div>
8
+ </div>
9
+ </div>
10
+ <div class="mt-8 flow-root">
11
+ <%= turbo_frame_tag "frame", src: exceptions_path(frame: true), data: {"eyeloupe--refresh-target": "frame"} do %><% end %>
12
+ </div>
13
+ </div>
@@ -0,0 +1,126 @@
1
+ <div class="px-4 sm:px-6 lg:px-8 bg-white rounded-md shadow-md py-5">
2
+ <div class="px-4 sm:px-0">
3
+ <h3 class="text-xl font-semibold leading-7 text-gray-900">Exception Details</h3>
4
+ </div>
5
+ <div class="mt-6 border-t border-gray-100">
6
+ <dl class="divide-y divide-gray-100">
7
+ <div class="px-4 py-6 sm:grid sm:grid-cols-3 sm:gap-4 sm:px-0">
8
+ <dt class="text-base font-medium leading-6 text-gray-900">Time</dt>
9
+ <dd class="mt-1 text-base leading-6 text-gray-700 sm:col-span-2 sm:mt-0">
10
+ <%= @exception.created_at.to_formatted_s(:long) %> (<%= distance_of_time_in_words(@exception.updated_at, DateTime.now) %>)
11
+ </dd>
12
+ </div>
13
+ <div class="px-4 py-6 sm:grid sm:grid-cols-3 sm:gap-4 sm:px-0">
14
+ <dt class="text-base font-medium leading-6 text-gray-900">Hostname</dt>
15
+ <dd class="mt-1 text-base leading-6 text-gray-700 sm:col-span-2 sm:mt-0"><%= @exception.hostname %></dd>
16
+ </div>
17
+ <div class="px-4 py-6 sm:grid sm:grid-cols-3 sm:gap-4 sm:px-0">
18
+ <dt class="text-base font-medium leading-6 text-gray-900">Kind</dt>
19
+ <dd class="mt-1 text-base leading-6 text-gray-700 sm:col-span-2 sm:mt-0">
20
+ <%= @exception.kind %>
21
+ </dd>
22
+ </div>
23
+ <div class="px-4 py-6 sm:grid sm:grid-cols-3 sm:gap-4 sm:px-0">
24
+ <dt class="text-base font-medium leading-6 text-gray-900">Count</dt>
25
+ <dd class="mt-1 text-base leading-6 text-gray-700 sm:col-span-2 sm:mt-0">
26
+ <%= @exception.count %>
27
+ </dd>
28
+ </div>
29
+ <% if @exception.in_request_id.present? %>
30
+ <div class="px-4 py-6 sm:grid sm:grid-cols-3 sm:gap-4 sm:px-0">
31
+ <dt class="text-base font-medium leading-6 text-gray-900">Request</dt>
32
+ <dd class="mt-1 text-base leading-6 text-gray-700 sm:col-span-2 sm:mt-0">
33
+ <%= link_to "View request", in_request_path(@exception.in_request_id), class: "underline" %>
34
+ </dd>
35
+ </div>
36
+ <% end %>
37
+ <% if @exception.out_request_id.present? %>
38
+ <div class="px-4 py-6 sm:grid sm:grid-cols-3 sm:gap-4 sm:px-0">
39
+ <dt class="text-base font-medium leading-6 text-gray-900">Request</dt>
40
+ <dd class="mt-1 text-base leading-6 text-gray-700 sm:col-span-2 sm:mt-0">
41
+ <%= link_to "View request", out_request_path(@exception.out_request_id), class: "underline" %>
42
+ </dd>
43
+ </div>
44
+ <% end %>
45
+ <div class="px-4 py-6 sm:grid sm:grid-cols-3 sm:gap-4 sm:px-0">
46
+ <dt class="text-base font-medium leading-6 text-gray-900">Message</dt>
47
+ <dd class="mt-1 text-base leading-6 sm:col-span-2 sm:mt-0 rounded-md bg-black text-white overflow-x-auto">
48
+ <pre class="p-2"><%= @exception.message %></pre>
49
+ </dd>
50
+ </div>
51
+ <div class="px-4 py-6 sm:grid sm:grid-cols-3 sm:gap-4 sm:px-0">
52
+ <dt class="text-base font-medium leading-6 text-gray-900">Backtrace</dt>
53
+ <dd class="mt-1 text-base leading-6 sm:col-span-2 sm:mt-0 rounded-md bg-black text-white overflow-x-auto">
54
+ <pre class="p-2"><%= JSON.parse(@exception.stacktrace).join("\n") %></pre>
55
+ </dd>
56
+ </div>
57
+ <div class="px-4 py-6 sm:grid sm:grid-cols-3 sm:gap-4 sm:px-0">
58
+ <dt class="text-base font-medium leading-6 text-gray-900">Code preview</dt>
59
+ <dd class="mt-1 text-base leading-6 sm:col-span-2 sm:mt-0 rounded-md bg-black text-white overflow-x-auto px-2 pt-2">
60
+ <p class="pb-4 font-semibold"><%= @exception.file %></p>
61
+ <% JSON.parse(@exception.location).each_with_index do |line, i| %>
62
+ <div class="grid grid-cols-12 gap-x-1 <%= @line_numbers[i] + 1 == @exception.line ? "bg-red-500 bg-opacity-30" : "" %>">
63
+ <span class="text-right col-span-1 font-medium text-base text-gray-300"><%= @line_numbers[i] + 1 %></span>
64
+ <pre class="col-span-11"><code class="!p-0"><%= line %></code></pre>
65
+ </div>
66
+ <% end %>
67
+ </dd>
68
+ </div>
69
+ <% if Eyeloupe::configuration.openai_access_key.present? %>
70
+ <div class="px-4 py-6 sm:grid sm:grid-cols-3 sm:gap-4 sm:px-0" data-controller="eyeloupe--ai-assistant" data-eyeloupe--ai-assistant-url-value="<%= ai_assistant_response_url(@exception) %>">
71
+ <dt class="text-base font-medium leading-6 text-gray-900">AI Assistant</dt>
72
+ <dd class="mt-1 text-base leading-6 text-gray-700 sm:col-span-2 sm:mt-0">
73
+ <button data-action="eyeloupe--ai-assistant#assist" data-eyeloupe--ai-assistant-target="btn" class="rounded-md px-2 py-1 text-white bg-red-500 text-xl hover:shadow-md hover:bg-red-400 transition flex gap-x-2">
74
+ <svg xmlns="http://www.w3.org/2000/svg" class="icon icon-tabler icon-tabler-robot" width="24" height="24" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round">
75
+ <path stroke="none" d="M0 0h24v24H0z" fill="none"></path>
76
+ <path d="M7 7h10a2 2 0 0 1 2 2v1l1 1v3l-1 1v3a2 2 0 0 1 -2 2h-10a2 2 0 0 1 -2 -2v-3l-1 -1v-3l1 -1v-1a2 2 0 0 1 2 -2z"></path>
77
+ <path d="M10 16h4"></path>
78
+ <circle cx="8.5" cy="11.5" r=".5" fill="currentColor"></circle>
79
+ <circle cx="15.5" cy="11.5" r=".5" fill="currentColor"></circle>
80
+ <path d="M9 7l-1 -4"></path>
81
+ <path d="M15 7l1 -4"></path>
82
+ </svg>
83
+ <span>Click to get some help</span>
84
+ </button>
85
+
86
+ <svg data-eyeloupe--ai-assistant-target="loader" width="50" class="hidden" viewBox="0 0 120 30" xmlns="http://www.w3.org/2000/svg" fill="#ff3130">
87
+ <circle cx="15" cy="15" r="15">
88
+ <animate attributeName="r" from="15" to="15"
89
+ begin="0s" dur="0.8s"
90
+ values="15;9;15" calcMode="linear"
91
+ repeatCount="indefinite" />
92
+ <animate attributeName="fill-opacity" from="1" to="1"
93
+ begin="0s" dur="0.8s"
94
+ values="1;.5;1" calcMode="linear"
95
+ repeatCount="indefinite" />
96
+ </circle>
97
+ <circle cx="60" cy="15" r="9" fill-opacity="0.3">
98
+ <animate attributeName="r" from="9" to="9"
99
+ begin="0s" dur="0.8s"
100
+ values="9;15;9" calcMode="linear"
101
+ repeatCount="indefinite" />
102
+ <animate attributeName="fill-opacity" from="0.5" to="0.5"
103
+ begin="0s" dur="0.8s"
104
+ values=".5;1;.5" calcMode="linear"
105
+ repeatCount="indefinite" />
106
+ </circle>
107
+ <circle cx="105" cy="15" r="15">
108
+ <animate attributeName="r" from="15" to="15"
109
+ begin="0s" dur="0.8s"
110
+ values="15;9;15" calcMode="linear"
111
+ repeatCount="indefinite" />
112
+ <animate attributeName="fill-opacity" from="1" to="1"
113
+ begin="0s" dur="0.8s"
114
+ values="1;.5;1" calcMode="linear"
115
+ repeatCount="indefinite" />
116
+ </circle>
117
+ </svg>
118
+
119
+ <p data-eyeloupe--ai-assistant-target="result" id="result"></p>
120
+ </dd>
121
+ </div>
122
+ <% end %>
123
+ </dl>
124
+ </div>
125
+
126
+ </div>
@@ -21,7 +21,7 @@
21
21
  <td class="whitespace-nowrap py-4 pl-4 pr-3 text-base font-medium text-gray-900 sm:pl-0">
22
22
  <%= render "eyeloupe/shared/verb", verb: request.verb %>
23
23
  </td>
24
- <td class="whitespace-nowrap px-3 py-4 text-base text-gray-500"><%= request.path.truncate(50) %></td>
24
+ <td class="whitespace-nowrap px-3 py-4 text-base text-gray-500"><%= request.path.truncate(100) %></td>
25
25
  <td class="whitespace-nowrap px-3 py-4 text-base text-gray-500">
26
26
  <%= render "eyeloupe/shared/status_code", code: request.status %>
27
27
  </td>
@@ -1,4 +1,4 @@
1
- <div data-controller="eyeloupe--search">
1
+ <div data-controller="eyeloupe--search" class="px-4 sm:px-6 lg:px-8 bg-white rounded-md shadow-md py-5">
2
2
  <div class="sm:flex sm:items-center">
3
3
  <div class="sm:flex-auto">
4
4
  <h1 class="text-xl font-semibold leading-6 text-gray-900">Requests</h1>
@@ -1,4 +1,4 @@
1
- <div>
1
+ <div class="px-4 sm:px-6 lg:px-8 bg-white rounded-md shadow-md py-5">
2
2
  <div class="px-4 sm:px-0">
3
3
  <h3 class="text-xl font-semibold leading-7 text-gray-900">Request details</h3>
4
4
  </div>
@@ -80,3 +80,19 @@
80
80
  </dl>
81
81
  </div>
82
82
  </div>
83
+
84
+ <% if @request.exception.present? %>
85
+ <div class="mt-6 bg-white rounded-md shadow-md py-5">
86
+ <h3 class="text-base font-semibold leading-6 text-gray-900 px-4 sm:px-6 lg:px-8 border-b border-gray-100 pb-3">Exceptions</h3>
87
+
88
+ <%= link_to exception_path(@request.exception), class: "text-gray-600 hover:text-gray-900 hover:bg-gray-100 block px-4 sm:px-6 lg:px-8", data: {"turbo_frame": "_top"} do %>
89
+ <div class="divide-y divide-gray-100 px-0 flex justify-between">
90
+ <div class="whitespace-nowrap py-4 pl-4 pr-3 text-base font-medium text-gray-900 sm:pl-0">
91
+ <%= @request.exception.kind %>
92
+ <p class="text-sm font-normal text-gray-500"><%= @request.exception.message %></p>
93
+ </div>
94
+ </div>
95
+ <% end %>
96
+
97
+ </div>
98
+ <% end %>
@@ -23,7 +23,7 @@
23
23
  <%= render "eyeloupe/shared/verb", verb: request.verb %>
24
24
  </td>
25
25
  <td class="whitespace-nowrap px-3 py-4 text-base text-gray-500"><%= request.hostname %></td>
26
- <td class="whitespace-nowrap px-3 py-4 text-base text-gray-500"><%= request.path.truncate(50) %></td>
26
+ <td class="whitespace-nowrap px-3 py-4 text-base text-gray-500"><%= request.path.truncate(100) %></td>
27
27
  <td class="whitespace-nowrap px-3 py-4 text-base text-gray-500">
28
28
  <%= render "eyeloupe/shared/status_code", code: request.status %>
29
29
  </td>
@@ -1,4 +1,4 @@
1
- <div data-controller="eyeloupe--search">
1
+ <div data-controller="eyeloupe--search" class="px-4 sm:px-6 lg:px-8 bg-white rounded-md shadow-md py-5">
2
2
  <div class="sm:flex sm:items-center">
3
3
  <div class="sm:flex-auto">
4
4
  <h1 class="text-xl font-semibold leading-6 text-gray-900">HTTP Client</h1>
@@ -1,4 +1,4 @@
1
- <div>
1
+ <div class="px-4 sm:px-6 lg:px-8 bg-white rounded-md shadow-md py-5">
2
2
  <div class="px-4 sm:px-0">
3
3
  <h3 class="text-xl font-semibold leading-7 text-gray-900">HTTP Client Request Details</h3>
4
4
  </div>
@@ -67,3 +67,19 @@
67
67
  </dl>
68
68
  </div>
69
69
  </div>
70
+
71
+ <% if @request.exception.present? %>
72
+ <div class="mt-6 bg-white rounded-md shadow-md py-5">
73
+ <h3 class="text-base font-semibold leading-6 text-gray-900 px-4 sm:px-6 lg:px-8 border-b border-gray-100 pb-3">Exceptions</h3>
74
+
75
+ <%= link_to exception_path(@request.exception), class: "text-gray-600 hover:text-gray-900 hover:bg-gray-100 block px-4 sm:px-6 lg:px-8", data: {"turbo_frame": "_top"} do %>
76
+ <div class="divide-y divide-gray-100 px-0 flex justify-between">
77
+ <div class="whitespace-nowrap py-4 pl-4 pr-3 text-base font-medium text-gray-900 sm:pl-0">
78
+ <%= @request.exception.kind %>
79
+ <p class="text-sm font-normal text-gray-500"><%= @request.exception.message %></p>
80
+ </div>
81
+ </div>
82
+ <% end %>
83
+
84
+ </div>
85
+ <% end %>
@@ -67,6 +67,23 @@
67
67
  HTTP Client
68
68
  <% end %>
69
69
  </li>
70
+ <li>
71
+ <%= link_to exceptions_path, class:"#{request.path.include?('/exceptions') ? 'bg-gray-200 text-red-500' : ''} hover:bg-gray-200 text-gray-500 group flex gap-x-3 rounded-md p-2 text-base leading-6 font-medium" do %>
72
+ <svg xmlns="http://www.w3.org/2000/svg" class="h-6 w-6 shrink-0" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round">
73
+ <path stroke="none" d="M0 0h24v24H0z" fill="none"></path>
74
+ <path d="M9 9v-1a3 3 0 0 1 6 0v1"></path>
75
+ <path d="M8 9h8a6 6 0 0 1 1 3v3a5 5 0 0 1 -10 0v-3a6 6 0 0 1 1 -3"></path>
76
+ <path d="M3 13l4 0"></path>
77
+ <path d="M17 13l4 0"></path>
78
+ <path d="M12 20l0 -6"></path>
79
+ <path d="M4 19l3.35 -2"></path>
80
+ <path d="M20 19l-3.35 -2"></path>
81
+ <path d="M4 7l3.75 2.4"></path>
82
+ <path d="M20 7l-3.75 2.4"></path>
83
+ </svg>
84
+ Exceptions
85
+ <% end %>
86
+ </li>
70
87
  </ul>
71
88
  </li>
72
89
  </ul>
@@ -84,7 +101,7 @@
84
101
  <%= image_tag "eyeloupe/logo.png", class: "h-12 w-auto" %>
85
102
  <h1 class="ml-2 text-2xl font-semibold text-gray-700">Eyeloupe</h1>
86
103
  <% end %>
87
- <nav class="flex flex-1 flex-col">
104
+ <nav class="flex flex-1 flex-col mt-4">
88
105
  <ul role="list" class="flex flex-1 flex-col gap-y-7">
89
106
  <li>
90
107
  <ul role="list" class="-mx-2 space-y-1">
@@ -114,6 +131,23 @@
114
131
  HTTP Client
115
132
  <% end %>
116
133
  </li>
134
+ <li>
135
+ <%= link_to exceptions_path, class:"#{request.path.include?('/exceptions') ? 'bg-gray-200 text-red-500' : ''} hover:bg-gray-200 text-gray-500 group flex gap-x-3 rounded-md p-2 text-base leading-6 font-medium" do %>
136
+ <svg xmlns="http://www.w3.org/2000/svg" class="h-6 w-6 shrink-0" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round">
137
+ <path stroke="none" d="M0 0h24v24H0z" fill="none"></path>
138
+ <path d="M9 9v-1a3 3 0 0 1 6 0v1"></path>
139
+ <path d="M8 9h8a6 6 0 0 1 1 3v3a5 5 0 0 1 -10 0v-3a6 6 0 0 1 1 -3"></path>
140
+ <path d="M3 13l4 0"></path>
141
+ <path d="M17 13l4 0"></path>
142
+ <path d="M12 20l0 -6"></path>
143
+ <path d="M4 19l3.35 -2"></path>
144
+ <path d="M20 19l-3.35 -2"></path>
145
+ <path d="M4 7l3.75 2.4"></path>
146
+ <path d="M20 7l-3.75 2.4"></path>
147
+ </svg>
148
+ Exceptions
149
+ <% end %>
150
+ </li>
117
151
  </ul>
118
152
  </li>
119
153
  </ul>
@@ -191,11 +225,13 @@
191
225
  </div>
192
226
 
193
227
  <main class="py-10 lg:pl-72 px-5 mx-auto">
194
-
195
- <div class="px-4 sm:px-6 lg:px-8 bg-white rounded-md shadow-md py-5">
196
- <%= yield %>
197
- </div>
228
+ <%= yield %>
198
229
  </main>
230
+
231
+ <footer class="text-gray-500 py-1 lg:pl-72 px-5 mx-auto text-sm">
232
+ Eyeloupe <%= Eyeloupe::VERSION %> - <a class="underline" href="https://github.com/alxlion/eyeloupe">Github</a>
233
+ </footer>
234
+
199
235
  </div>
200
236
  </div>
201
237
 
data/config/importmap.rb CHANGED
@@ -1,4 +1,5 @@
1
1
  pin_all_from File.expand_path("../app/assets/javascripts", __dir__)
2
2
  pin "@hotwired/turbo-rails", to: "turbo.min.js", preload: true
3
3
  pin "@hotwired/stimulus", to: "https://ga.jspm.io/npm:@hotwired/stimulus@3.0.1/dist/stimulus.js"
4
- pin "@hotwired/stimulus-loading", to: "stimulus-loading.js", preload: true
4
+ pin "@hotwired/stimulus-loading", to: "stimulus-loading.js", preload: true
5
+ pin "showdown", to: "https://ga.jspm.io/npm:showdown@2.1.0/dist/showdown.js"
data/config/routes.rb CHANGED
@@ -4,6 +4,8 @@ Eyeloupe::Engine.routes.draw do
4
4
 
5
5
  resources :in_requests, only: [:index, :show]
6
6
  resources :out_requests, only: [:index, :show]
7
+ resources :exceptions, only: [:index, :show]
8
+ resources :ai_assistant_responses, only: [:show]
7
9
 
8
10
  resource :data, only: [:destroy]
9
11
 
@@ -0,0 +1,21 @@
1
+ class CreateEyeloupeExceptions < ActiveRecord::Migration[7.0]
2
+ def change
3
+ create_table :eyeloupe_exceptions do |t|
4
+ t.string :hostname
5
+ t.string :kind
6
+ t.string :location
7
+ t.string :file
8
+ t.integer :line
9
+ t.text :stacktrace
10
+ t.string :message
11
+ t.integer :count, default: 1
12
+ t.text :full_message
13
+ t.references :in_request, null: true, foreign_key: { to_table: :eyeloupe_in_requests }
14
+ t.references :out_request, null: true, foreign_key: { to_table: :eyeloupe_out_requests }
15
+
16
+ t.timestamps
17
+ end
18
+
19
+ add_index :eyeloupe_exceptions, [:kind, :file, :line]
20
+ end
21
+ end
@@ -0,0 +1,14 @@
1
+ module Eyeloupe
2
+ module Concerns
3
+ module Rescuable
4
+ extend ActiveSupport::Concern
5
+
6
+ included do
7
+ rescue_from(StandardError) do |exception|
8
+ Eyeloupe::Processors::Exception.instance.process(nil, exception)
9
+ end
10
+ end
11
+
12
+ end
13
+ end
14
+ end
@@ -11,9 +11,16 @@ module Eyeloupe
11
11
  # @return [Boolean]
12
12
  attr_accessor :capture
13
13
 
14
+ # @return [String]
15
+ attr_accessor :openai_access_key
16
+
17
+ # @return [String]
18
+ attr_accessor :openai_model
19
+
14
20
  def initialize
15
21
  @excluded_paths = %w[]
16
22
  @capture = true
23
+ @openai_model = "gpt-3.5-turbo"
17
24
  end
18
25
  end
19
26
 
@@ -12,6 +12,52 @@ module Eyeloupe
12
12
  app.config.middleware.insert(0, Eyeloupe::RequestMiddleware)
13
13
  end
14
14
 
15
+ initializer 'eyeloupe.active_job' do
16
+ ActiveSupport.on_load(:active_job) do
17
+ include Eyeloupe::Concerns::Rescuable
18
+ end
19
+ end
20
+
21
+ initializer "eyeloupe.configure_openai" do
22
+ OpenAI.configure do |config|
23
+ config.access_token = Eyeloupe::configuration.openai_access_key
24
+ end
25
+ end
26
+
27
+ initializer "eyeloupe.configure_sidekiq" do
28
+ if defined?(Sidekiq)
29
+ Sidekiq.configure_server do |config|
30
+ config.error_handlers << proc {|ex,ctx_hash| Eyeloupe::Processors::Exception.instance.process(nil, ex) }
31
+ end
32
+ end
33
+ end
34
+
35
+ initializer 'eyeloupe.override_net_http_request' do
36
+ require 'net/http'
37
+ Net::HTTP.class_eval do
38
+ alias original_request request
39
+ def request(req, body = nil, &block)
40
+ res, ex = nil
41
+ exception_processor = Eyeloupe::Processors::Exception.instance
42
+ out_request_processor = Eyeloupe::Processors::OutRequest.instance
43
+
44
+ if Eyeloupe.configuration.capture
45
+ begin
46
+ out_request_processor.init(req, body)
47
+ res = original_request(req, body, &block)
48
+ rescue => e
49
+ ex = exception_processor.process(nil, e)
50
+ ensure
51
+ out_request_processor.process(res, ex)
52
+ end
53
+ else
54
+ res = original_request(req, body, &block)
55
+ end
56
+ res
57
+ end
58
+ end
59
+ end
60
+
15
61
  initializer "eyeloupe.importmap", :before => "importmap" do |app|
16
62
  app.config.importmap.paths << root.join("config/importmap.rb")
17
63
  # https://github.com/rails/importmap-rails#sweeping-the-cache-in-development-and-test
@@ -0,0 +1,71 @@
1
+ require 'socket'
2
+ module Eyeloupe
3
+ module Processors
4
+ class Exception
5
+ include Singleton
6
+
7
+ # @param [Hash, nil] env Rack environment
8
+ # @param [Exception] exception The exception object
9
+ # @return [Eyeloupe::Exception] The exception model
10
+ def process(env, exception)
11
+ if env && env['action_dispatch.backtrace_cleaner'].present?
12
+ backtrace = env['action_dispatch.backtrace_cleaner'].filter(exception.backtrace)
13
+ backtrace = exception.backtrace if backtrace.blank?
14
+ else
15
+ backtrace = exception.backtrace
16
+ end
17
+
18
+ file = backtrace ? backtrace[0].split(":")[0] : ""
19
+ line = backtrace ? backtrace[0].split(":")[1].to_i : 0
20
+
21
+ create_or_update_exception(exception.class.name || "", file, line, backtrace, exception.message, exception.full_message)
22
+ end
23
+
24
+ protected
25
+
26
+ # @param [Array] trace The backtrace
27
+ # @return [Array] The source code lines
28
+ def read_file(trace)
29
+ file = trace.size > 0 ? trace[0].split(":")[0] : ""
30
+ line = trace.size > 0 ? trace[0].split(":")[1].to_i : 0
31
+
32
+ if File.exist?(file)
33
+ lines = File.readlines(file)
34
+ start = line - 5
35
+ start = 0 if start < 0
36
+ lines[start..line+5] || []
37
+ else
38
+ []
39
+ end
40
+ end
41
+
42
+ # @param [String] kind The exception class name
43
+ # @param [String] file The file path
44
+ # @param [Integer] line The line number
45
+ # @param [Array] backtrace The backtrace
46
+ # @param [String] message The exception message
47
+ # @param [String] full_message The full exception message
48
+ # @return [Eyeloupe::Exception] The exception model
49
+ def create_or_update_exception(kind, file, line, backtrace, message, full_message)
50
+ obj = Eyeloupe::Exception.find_by(kind: kind, file: file, line: line)
51
+
52
+ if obj
53
+ obj.update(count: obj.count + 1, updated_at: Time.now)
54
+ else
55
+ obj = Eyeloupe::Exception.create(
56
+ hostname: Socket.gethostname,
57
+ kind: kind,
58
+ message: message,
59
+ full_message: full_message,
60
+ location: read_file(backtrace || []).to_json,
61
+ file: file,
62
+ line: line,
63
+ stacktrace: (backtrace || []).to_json,
64
+ )
65
+ end
66
+
67
+ obj
68
+ end
69
+ end
70
+ end
71
+ end
@@ -31,6 +31,9 @@ module Eyeloupe
31
31
  # @return [Array]
32
32
  attr_accessor :subs
33
33
 
34
+ # @return [Eyeloupe::Exception, nil]
35
+ attr_accessor :ex
36
+
34
37
  def initialize
35
38
  @env = {}
36
39
  @request = ActionDispatch::Request.new(@env)
@@ -40,6 +43,7 @@ module Eyeloupe
40
43
  @timings = {}
41
44
  @started_at = nil
42
45
  @subs = []
46
+ @ex = nil
43
47
  end
44
48
 
45
49
  # @param [ActionDispatch::Request] request The request object
@@ -47,8 +51,9 @@ module Eyeloupe
47
51
  # @param [Integer, nil] status HTTP status code
48
52
  # @param [Hash, nil] headers HTTP headers
49
53
  # @param [String, nil] response HTTP response
54
+ # @param [Eyeloupe::Exception, nil] ex The exception object persisted in db
50
55
  # @return [Eyeloupe::Processors::InRequest]
51
- def init(request, env, status, headers, response)
56
+ def init(request, env, status, headers, response, ex)
52
57
  unsubscribe
53
58
 
54
59
  @request = request
@@ -56,6 +61,7 @@ module Eyeloupe
56
61
  @status = status
57
62
  @headers = headers
58
63
  @response = response
64
+ @ex = ex
59
65
 
60
66
  self
61
67
  end
@@ -72,7 +78,8 @@ module Eyeloupe
72
78
 
73
79
  # @return [Eyeloupe::InRequest]
74
80
  def process
75
- Eyeloupe::InRequest.create(
81
+
82
+ req = Eyeloupe::InRequest.create(
76
83
  verb: @request.request_method,
77
84
  hostname: @request.host,
78
85
  path: @env["REQUEST_URI"],
@@ -88,6 +95,10 @@ module Eyeloupe
88
95
  session: (@request.session || {}).to_json,
89
96
  response: get_response,
90
97
  )
98
+
99
+ @ex.update(in_request_id: req.id) if @ex.present? && @ex.in_request_id.blank?
100
+
101
+ req
91
102
  end
92
103
 
93
104
  protected
@@ -105,7 +116,7 @@ module Eyeloupe
105
116
 
106
117
  # @return [String, nil]
107
118
  def get_response
108
- if @request.format.to_s =~ /html/
119
+ if @request.format.to_s =~ /html/ || @headers&.to_json =~ /html/
109
120
  "HTML content"
110
121
  elsif @response.is_a?(ActionDispatch::Response)
111
122
  @response.body
@@ -11,25 +11,13 @@ module Eyeloupe
11
11
  # @return [String]
12
12
  attr_accessor :body
13
13
 
14
- # @return [Hash]
15
- attr_accessor :req_headers
16
-
17
- # @return [Hash]
18
- attr_accessor :res_headers
19
-
20
- # @return [Net::HTTPResponse, nil]
21
- attr_accessor :response
22
-
23
14
  # @return [Time, nil]
24
15
  attr_accessor :started_at
25
16
 
26
17
  def initialize
27
18
  @request = nil
28
19
  @body = ""
29
- @req_headers = {}
30
- @res_headers = {}
31
20
  @started_at = nil
32
- @response = nil
33
21
  end
34
22
 
35
23
  # @param [Net::HTTPRequest] request The request object
@@ -41,22 +29,24 @@ module Eyeloupe
41
29
  end
42
30
 
43
31
  # @param [Net::HTTPResponse] response The response object
32
+ # @param [Eyeloupe::Exception, nil] ex The exception object persisted in db
44
33
  # @return [Net::HTTPResponse] The response object
45
- def process(response)
46
- @response = response
47
-
48
- Eyeloupe::OutRequest.create(
34
+ def process(response, ex)
35
+ req = Eyeloupe::OutRequest.create(
49
36
  verb: @request.method,
50
37
  hostname: @request['host'],
51
38
  path: @request.path,
52
- status: @response.code,
53
- format: @response.content_type,
39
+ status: response.code,
40
+ format: response.content_type,
54
41
  duration: (Time.now - @started_at) * 1000,
55
42
  payload: @request.body,
56
43
  req_headers: (get_headers(@request) || {}).to_json,
57
- res_headers: (get_headers(@response) || {}).to_json,
58
- response: @response.body,
44
+ res_headers: (get_headers(response) || {}).to_json,
45
+ response: response.body,
59
46
  )
47
+
48
+ ex.update(out_request_id: req.id) if ex.present? && ex.out_request_id.blank?
49
+
60
50
  response
61
51
  end
62
52
 
@@ -5,30 +5,42 @@ module Eyeloupe
5
5
  # @return [Eyeloupe::Processors::InRequest]
6
6
  attr_accessor :inrequest_processor
7
7
 
8
+ # @return [Eyeloupe::Processors::Exception]
9
+ attr_accessor :exception_processor
10
+
8
11
  def initialize(app)
9
12
  @app = app
10
13
  @inrequest_processor = Processors::InRequest.instance
14
+ @exception_processor = Processors::Exception.instance
11
15
  end
12
16
 
13
17
  # @param [Hash] env Rack environment
14
18
  def call(env)
15
19
 
16
20
  request = ActionDispatch::Request.new(env)
21
+ ex = nil
17
22
 
18
23
  if enabled?(request) && !skip_request?(request)
19
24
  @inrequest_processor.start_timer
20
25
 
21
26
  begin
22
27
  status, headers, response = @app.call(env)
28
+
29
+ framework_exception = env['action_dispatch.exception']
30
+ if framework_exception
31
+ ex = @exception_processor.process(env, framework_exception)
32
+ end
33
+
23
34
  [status, headers, response]
24
35
  rescue Exception => e
25
36
  exception = ActionDispatch::ExceptionWrapper.new(env, e)
26
37
  status = exception.status_code
27
38
  headers = {}
28
39
  response = e.message
40
+ ex = @exception_processor.process(env, e)
29
41
  raise
30
42
  ensure
31
- @inrequest_processor.init(request, env, status, headers, response).process
43
+ @inrequest_processor.init(request, env, status, headers, response, ex).process
32
44
  end
33
45
  else
34
46
  @app.call(env)
@@ -1,4 +1,4 @@
1
1
  module Eyeloupe
2
2
  # @return [String]
3
- VERSION = "0.2.0"
3
+ VERSION = "0.3.0"
4
4
  end
data/lib/eyeloupe.rb CHANGED
@@ -3,11 +3,13 @@ require "eyeloupe/engine"
3
3
 
4
4
  require 'eyeloupe/request_middleware'
5
5
  require 'eyeloupe/configuration'
6
- require 'eyeloupe/http'
7
6
  require 'eyeloupe/processors/in_request'
8
7
  require 'eyeloupe/processors/out_request'
8
+ require 'eyeloupe/processors/exception'
9
+ require 'eyeloupe/concerns/rescuable'
9
10
 
10
11
  require 'pagy'
12
+ require "openai"
11
13
  module Eyeloupe
12
14
 
13
15
  # @return [Eyeloupe::Configuration]
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: eyeloupe
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.2.0
4
+ version: 0.3.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Alexandre Lion
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2023-06-04 00:00:00.000000000 Z
11
+ date: 2023-06-17 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: sprockets-rails
@@ -66,6 +66,20 @@ dependencies:
66
66
  - - "~>"
67
67
  - !ruby/object:Gem::Version
68
68
  version: '6.0'
69
+ - !ruby/object:Gem::Dependency
70
+ name: ruby-openai
71
+ requirement: !ruby/object:Gem::Requirement
72
+ requirements:
73
+ - - "~>"
74
+ - !ruby/object:Gem::Version
75
+ version: 4.1.0
76
+ type: :runtime
77
+ prerelease: false
78
+ version_requirements: !ruby/object:Gem::Requirement
79
+ requirements:
80
+ - - "~>"
81
+ - !ruby/object:Gem::Version
82
+ version: 4.1.0
69
83
  - !ruby/object:Gem::Dependency
70
84
  name: sqlite3
71
85
  requirement: !ruby/object:Gem::Requirement
@@ -125,6 +139,7 @@ files:
125
139
  - app/assets/images/eyeloupe/logo.png
126
140
  - app/assets/javascripts/eyeloupe/application.js
127
141
  - app/assets/javascripts/eyeloupe/controllers/application.js
142
+ - app/assets/javascripts/eyeloupe/controllers/eyeloupe/ai_assistant_controller.js
128
143
  - app/assets/javascripts/eyeloupe/controllers/eyeloupe/nav_controller.js
129
144
  - app/assets/javascripts/eyeloupe/controllers/eyeloupe/pause_controller.js
130
145
  - app/assets/javascripts/eyeloupe/controllers/eyeloupe/refresh_controller.js
@@ -133,17 +148,23 @@ files:
133
148
  - app/assets/stylesheets/application.tailwind.css
134
149
  - app/assets/stylesheets/eyeloupe/application.css
135
150
  - app/controllers/concerns/eyeloupe/searchable.rb
151
+ - app/controllers/eyeloupe/ai_assistant_responses_controller.rb
136
152
  - app/controllers/eyeloupe/application_controller.rb
137
153
  - app/controllers/eyeloupe/configs_controller.rb
138
154
  - app/controllers/eyeloupe/data_controller.rb
155
+ - app/controllers/eyeloupe/exceptions_controller.rb
139
156
  - app/controllers/eyeloupe/in_requests_controller.rb
140
157
  - app/controllers/eyeloupe/out_requests_controller.rb
141
158
  - app/helpers/eyeloupe/application_helper.rb
142
159
  - app/jobs/eyeloupe/application_job.rb
143
160
  - app/mailers/eyeloupe/application_mailer.rb
144
161
  - app/models/eyeloupe/application_record.rb
162
+ - app/models/eyeloupe/exception.rb
145
163
  - app/models/eyeloupe/in_request.rb
146
164
  - app/models/eyeloupe/out_request.rb
165
+ - app/views/eyeloupe/exceptions/_frame.html.erb
166
+ - app/views/eyeloupe/exceptions/index.html.erb
167
+ - app/views/eyeloupe/exceptions/show.html.erb
147
168
  - app/views/eyeloupe/in_requests/_frame.html.erb
148
169
  - app/views/eyeloupe/in_requests/index.html.erb
149
170
  - app/views/eyeloupe/in_requests/show.html.erb
@@ -158,10 +179,12 @@ files:
158
179
  - config/tailwind.config.js
159
180
  - db/migrate/20230518175305_create_eyeloupe_in_requests.rb
160
181
  - db/migrate/20230525125352_create_eyeloupe_out_requests.rb
182
+ - db/migrate/20230604190442_create_eyeloupe_exceptions.rb
161
183
  - lib/eyeloupe.rb
184
+ - lib/eyeloupe/concerns/rescuable.rb
162
185
  - lib/eyeloupe/configuration.rb
163
186
  - lib/eyeloupe/engine.rb
164
- - lib/eyeloupe/http.rb
187
+ - lib/eyeloupe/processors/exception.rb
165
188
  - lib/eyeloupe/processors/in_request.rb
166
189
  - lib/eyeloupe/processors/out_request.rb
167
190
  - lib/eyeloupe/request_middleware.rb
data/lib/eyeloupe/http.rb DELETED
@@ -1,19 +0,0 @@
1
- # frozen_string_literal: true
2
- require 'net/http'
3
- module Net
4
- class HTTP
5
- alias original_request request
6
-
7
- def request(req, body = nil, &block)
8
- if Eyeloupe.configuration.capture
9
- Eyeloupe::Processors::OutRequest.instance.init(req, body)
10
- res = original_request(req, body, &block)
11
- Eyeloupe::Processors::OutRequest.instance.process(res)
12
- else
13
- res = original_request(req, body, &block)
14
- end
15
- res
16
- end
17
-
18
- end
19
- end